// Copyright (c) 2021 Lars Pontoppidan. All rights reserved. // Use of this source code is governed by an MIT license // that can be found in the LICENSE file. module parser import toml.ast import toml.checker import toml.decoder import toml.util import toml.token import toml.scanner pub const ( all_formatting = [token.Kind.whitespace, .tab, .cr, .nl] space_formatting = [token.Kind.whitespace, .tab] keys_and_space_formatting = [token.Kind.whitespace, .tab, .minus, .bare, .quoted, .boolean, .number, .underscore] ) type DottedKey = []string // str returns the dotted key as a string. pub fn (dk DottedKey) str() string { return dk.join('.') } // starts_with returns true if the dotted key starts with the same key entries as `target`. fn (dk DottedKey) starts_with(target DottedKey) bool { if dk.len >= target.len { for i := 0; i < target.len; i++ { if dk[i] != target[i] { return false } } return true } return false } // has returns true if the array contains `target`. fn (a []DottedKey) has(target DottedKey) bool { for dk in a { if dk == target { return true } } return false } // Parser contains the necessary fields for keeping the state of the parsing process. pub struct Parser { pub: config Config mut: scanner &scanner.Scanner = unsafe { nil } prev_tok token.Token tok token.Token peek_tok token.Token tokens []token.Token // To be able to peek more than one token ahead. skip_next bool // The root map (map is called table in TOML world) root_map map[string]ast.Value root_map_key DottedKey explicit_declared []DottedKey explicit_declared_array_of_tables []DottedKey implicit_declared []DottedKey // Array of Tables state last_aot DottedKey last_aot_index int // Root of the tree ast_root &ast.Root = &ast.Root{} } // Config is used to configure a Parser instance. // `run_checks` is used to en- or disable running of the strict `checker.Checker` type checks. // `decode_values` is used to en- or disable decoding of values with the `decoder.Decoder`. pub struct Config { pub: scanner &scanner.Scanner = unsafe { nil } run_checks bool = true decode_values bool = true } // new_parser returns a new, stack allocated, `Parser`. pub fn new_parser(config Config) Parser { return Parser{ config: config scanner: config.scanner } } // init initializes the parser. pub fn (mut p Parser) init() ! { p.root_map = map[string]ast.Value{} p.tokens << p.scanner.scan()! p.next()! } // run_checker validates the parsed `ast.Value` nodes in the // the generated AST. fn (mut p Parser) run_checker() ! { if p.config.run_checks { chckr := checker.Checker{ scanner: p.scanner } chckr.check(p.root_map)! for comment in p.ast_root.comments { chckr.check_comment(comment)! } } } // run_decoder decodes values in the parsed `ast.Value` nodes in the // the generated AST. fn (mut p Parser) run_decoder() ! { if p.config.decode_values { dcoder := decoder.Decoder{ scanner: p.scanner } dcoder.decode(mut p.root_map)! } } // parse starts parsing the input and returns the root // of the generated AST. pub fn (mut p Parser) parse() !&ast.Root { p.init()! p.root_table()! p.run_checker()! p.run_decoder()! p.ast_root.table = p.root_map return p.ast_root } // next forwards the parser to the next token. fn (mut p Parser) next() ! { p.prev_tok = p.tok p.tok = p.peek_tok if p.tokens.len > 0 { p.peek_tok = p.tokens.first() p.tokens.delete(0) p.peek(1)! } else { p.peek(1)! p.peek_tok = p.tokens.first() p.tokens.delete(0) } } // peek peeks forward `n` tokens. // peek returns `.unknown` if it can not peek ahead long enough. fn (mut p Parser) peek(n int) !token.Token { if n < 0 { return error(@MOD + '.' + @STRUCT + '.' + @FN + ' peeking backwards is not supported.') } if n == 0 { return p.peek_tok } else { // n >= 1 if n <= p.tokens.len { return p.tokens[n - 1] } else { mut token_ := token.Token{} mut count := n - p.tokens.len util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'buffering ${count} tokens...') for token_.kind != .eof && count != 0 { token_ = p.scanner.scan()! p.tokens << token_ count-- } return token_ } } } // check forwards the parser to the next token if the current // token's `Kind` is equal that of `check_token`. fn (mut p Parser) check(check_token token.Kind) ! { if p.tok.kind == check_token { p.next()! } else { return error(@MOD + '.' + @STRUCT + '.' + @FN + ' expected token "${check_token}" but found "${p.tok.kind}" in this (excerpt): "...${p.excerpt()}..."') } } // peek_for_correct_line_ending_or_fail peeks past any formatting tokens // and return an error if the next token is not one of [.cr, .nl, .hash, .eof]. fn (mut p Parser) peek_for_correct_line_ending_or_fail() ! { // Disallow anything else than [.cr, .nl, .hash, .eof] after any space formatting. peek_tok, _ := p.peek_over(1, parser.space_formatting)! if peek_tok.kind !in [.cr, .nl, .hash, .eof] { p.next()! // Forward to the peek_tok return error(@MOD + '.' + @STRUCT + '.' + @FN + ' unexpected EOL "${p.tok.kind}" "${p.tok.lit}" expected one of [.cr, .nl, .hash, .eof] at this (excerpt): "...${p.excerpt()}..."') } } // check_one_of forwards the parser to the next token if the current // token's `Kind` can be found in `tokens`. Otherwise it returns an error. fn (mut p Parser) check_one_of(tokens []token.Kind) ! { if p.tok.kind in tokens { p.next()! } else { return error(@MOD + '.' + @STRUCT + '.' + @FN + ' expected one of ${tokens} but found "${p.tok.kind}" in this (excerpt): "...${p.excerpt()}..."') } } // ignore_while forwards the parser to the next token as long as the current // token's `Kind` can be found in `tokens`. This is helpful for ignoring // a stream of formatting tokens. fn (mut p Parser) ignore_while(tokens []token.Kind) { if p.tok.kind in tokens { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'ignoring "${p.tok.kind}" ...') p.next() or { return } p.ignore_while(tokens) } } // ignore_while_peek forwards the parser to the next token as long as `peek_tok` // token's `Kind` can be found in `tokens`. This is helpful for ignoring // a stream of formatting tokens. // In contrast to `ignore_while`, `ignore_while_peek` compares on `peek_tok` this is // sometimes necessary since not all parser calls forward using the `next()` call. fn (mut p Parser) ignore_while_peek(tokens []token.Kind) { for p.peek_tok.kind in tokens { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'eating "${p.tok.kind}" ...') p.next() or { return } } } // peek_over peeks ahead from token starting at `i` skipping over // any `token.Kind`s found in `tokens`. `peek_over` returns the next token *not* // found in `tokens`. fn (mut p Parser) peek_over(i int, tokens []token.Kind) !(token.Token, int) { mut peek_tok := p.peek_tok // Peek ahead as far as we can from token at `i` while the peeked // token is found in `tokens`. mut peek_i := i for peek_tok.kind in tokens { peek_tok = p.peek(peek_i)! peek_i++ } return peek_tok, peek_i } // is_at returns true if the token kind is equal to `expected_token`. fn (mut p Parser) is_at(expected_token token.Kind) bool { return p.tok.kind == expected_token } // expect will error if the token kind is not equal to `expected_token`. fn (mut p Parser) expect(expected_token token.Kind) ! { if p.tok.kind == expected_token { return } else { return error(@MOD + '.' + @STRUCT + '.' + @FN + ' expected token "${expected_token}" but found "${p.tok.kind}" in this text "...${p.excerpt()}..."') } } // build_abs_dotted_key returns the absolute dotted key path. fn (p Parser) build_abs_dotted_key(key DottedKey) DottedKey { if p.root_map_key.len > 0 { mut abs_dotted_key := DottedKey([]string{}) abs_dotted_key << p.root_map_key abs_dotted_key << key return abs_dotted_key } return key } // todo_msvc_astring2dkey worksaround a MSVC compile error. // TODO remove. fn todo_msvc_astring2dkey(s []string) DottedKey { return s } // check_explicitly_declared returns an error if `key` has been explicitly declared. fn (p Parser) check_explicitly_declared(key DottedKey) ! { if p.explicit_declared.len > 0 && p.explicit_declared.has(key) { return error(@MOD + '.' + @STRUCT + '.' + @FN + ' key `${key.str()}` is already explicitly declared. Unexpected redeclaration at "${p.tok.kind}" "${p.tok.lit}" in this (excerpt): "...${p.excerpt()}..."') } } // check_explicitly_declared_array_of_tables returns an error if `key` has been // explicitly declared as an array of tables. fn (p Parser) check_explicitly_declared_array_of_tables(key DottedKey) ! { if p.explicit_declared_array_of_tables.len > 0 && p.explicit_declared_array_of_tables.has(key) { return error(@MOD + '.' + @STRUCT + '.' + @FN + ' key `${key.str()}` is already an explicitly declared array of tables. Unexpected redeclaration at "${p.tok.kind}" "${p.tok.lit}" in this (excerpt): "...${p.excerpt()}..."') } } // check_implicitly_declared returns an error if `key` has been implicitly declared. fn (p Parser) check_implicitly_declared(key DottedKey) ! { if p.implicit_declared.len > 0 && p.implicit_declared.has(key) { return error(@MOD + '.' + @STRUCT + '.' + @FN + ' key `${key.str()}` is already implicitly declared. Unexpected redeclaration at "${p.tok.kind}" "${p.tok.lit}" in this (excerpt): "...${p.excerpt()}..."') } } // find_table returns a reference to a map if found in the *root* table given a "dotted" key (`a.b.c`). // If some segments of the key does not exist in the root table find_table will // allocate a new map for each segment. This behavior is needed because you can // reference maps by multiple keys "dotted" (separated by "." periods) in TOML documents. // See also `find_in_table`. pub fn (mut p Parser) find_table() !&map[string]ast.Value { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'locating "${p.root_map_key}" in map ${ptr_str(p.root_map)}') mut t := unsafe { &p.root_map } if p.root_map_key.len == 0 { return t } return p.find_in_table(mut t, p.root_map_key) } // allocate_table allocates all tables in "dotted" `key` (`a.b.c`) in the *root* table. pub fn (mut p Parser) allocate_table(key DottedKey) ! { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'allocating "${key}" in map ${ptr_str(p.root_map)}') mut t := unsafe { &p.root_map } if key.len == 0 { return } p.allocate_in_table(mut t, key)! } // sub_table_key returns the logic parts of a dotted key (`a.b.c`) for // use with the `find_sub_table` method. pub fn (mut p Parser) sub_table_key(key DottedKey) (DottedKey, DottedKey) { last := [key.last()] first := key[..key.len - 1] return first, last } // find_sub_table returns a reference to a map if found in the *root* table given a "dotted" key (`a.b.c`). // If some segments of the key does not exist in the input map find_sub_table will // allocate a new map for the segment. This behavior is needed because you can // reference maps by multiple keys "dotted" (separated by "." periods) in TOML documents. // See also `find_in_table`. pub fn (mut p Parser) find_sub_table(key DottedKey) !&map[string]ast.Value { mut ky := DottedKey([]string{}) ky << p.root_map_key ky << key if p.root_map_key.len == 0 { ky = key } util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'locating "${ky}" in map ${ptr_str(p.root_map)}') mut t := unsafe { &p.root_map } if ky.len == 0 { return t } return p.find_in_table(mut t, ky) } // find_in_table returns a reference to a map if found in `table` given a "dotted" key (`a.b.c`). // If some segments of the key does not exist in the input map find_in_table will // allocate a new map for the segment. This behavior is needed because you can // reference maps by multiple keys "dotted" (separated by "." periods) in TOML documents. pub fn (mut p Parser) find_in_table(mut table map[string]ast.Value, key DottedKey) !&map[string]ast.Value { // NOTE This code is the result of much trial and error. // I'm still not quite sure *exactly* why it works. All I can leave here is a hope // that this kind of minefield someday will be easier in V :) util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'locating "${key}" in map ${ptr_str(table)}') mut t := unsafe { &table } unsafe { for k in key { if val := t[k] { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'found key "${k}" in ${t.keys()}') if val is map[string]ast.Value { t = &(val as map[string]ast.Value) } else { return error(@MOD + '.' + @STRUCT + '.' + @FN + ' "${k}" in "${key}" is not a map but `${val.type_name()}`') } } else { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'no key "${k}" in "${key}" found, allocating new map at key "${k}" in map ${ptr_str(t)}"') t[k] = map[string]ast.Value{} t = &(t[k] as map[string]ast.Value) util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'allocated new map ${ptr_str(t)}"') } } } util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'returning map ${ptr_str(t)}"') return t } // find_array_of_tables returns an array if found in the root table based on the parser's // last encountered "Array Of Tables" key. // If the state key does not exist find_array_in_table will return an error. pub fn (mut p Parser) find_array_of_tables() ![]ast.Value { mut t := unsafe { &p.root_map } mut key := p.last_aot if key.len > 1 { key = DottedKey([key[0]]) } util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'locating "${key}" in map ${ptr_str(t)}') unsafe { if val := t[key.str()] { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'found key "${key}" in ${t.keys()}') if val is []ast.Value { arr := (val as []ast.Value) return arr } } } return error(@MOD + '.' + @STRUCT + '.' + @FN + 'no key `${key}` found in map ${ptr_str(t)}"') } // allocate_in_table allocates all tables in "dotted" `key` (`a.b.c`) in `table`. pub fn (mut p Parser) allocate_in_table(mut table map[string]ast.Value, key DottedKey) ! { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'allocating "${key}" in map ${ptr_str(table)}') mut t := unsafe { &table } unsafe { for k in key { if val := t[k] { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'found key "${k}" in ${t.keys()}') if val is map[string]ast.Value { t = &(val as map[string]ast.Value) } else { return error(@MOD + '.' + @STRUCT + '.' + @FN + ' "${k}" in "${key}" is not a map (${val.type_name()})') } } else { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'no key "${k}" in "${key}" found, allocating new map at key "${k}" in map ${ptr_str(t)}"') t[k] = map[string]ast.Value{} t = &(t[k] as map[string]ast.Value) util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'allocated new map ${ptr_str(t)}"') } } } } // dotted_key returns a string of the next tokens parsed as // sub/nested/path keys (e.g. `a.b.c`). In TOML, this form of key is referred to as a "dotted" key. pub fn (mut p Parser) dotted_key() !DottedKey { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'parsing dotted key...') mut dotted_key := DottedKey([]string{}) key := p.key()! p.ignore_while_peek(parser.space_formatting) dotted_key << key.str() for p.peek_tok.kind == .period { p.next()! // . p.check(.period)! p.ignore_while(parser.space_formatting) next_key := p.key()! dotted_key << next_key.text p.ignore_while_peek(parser.space_formatting) } p.next()! util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'parsed dotted key `${dotted_key}` now at "${p.tok.kind}" "${p.tok.lit}"') return dotted_key } // root_table parses next tokens into the root map of `ast.Value`s. // The V `map` type is corresponding to a "table" in TOML. pub fn (mut p Parser) root_table() ! { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'parsing root table...') for p.tok.kind != .eof { if !p.skip_next { p.next()! } else { p.skip_next = false } util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'parsing token "${p.tok.kind}" "${p.tok.lit}"') match p.tok.kind { .hash { c := p.comment() p.ast_root.comments << c util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'skipping comment "${c.text}"') } .whitespace, .tab, .nl, .cr { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'skipping formatting "${p.tok.kind}" "${p.tok.lit}"') continue } .bare, .quoted, .number, .minus, .underscore { // Peek forward as far as we can skipping over space formatting tokens. peek_tok, _ := p.peek_over(1, parser.keys_and_space_formatting)! if peek_tok.kind == .period { dotted_key, val := p.dotted_key_value()! sub_table, key := p.sub_table_key(dotted_key) // NOTE these are *relatively* costly checks. In general - and by specification, // TOML documents are expected to be "small" so this shouldn't be a problem. Famous last words. for explicit_key in p.explicit_declared { // Check for key re-defining: // https://github.com/iarna/toml-spec-tests/blob/1880b1a/errors/inline-table-imutable-1.toml if p.build_abs_dotted_key(sub_table) == explicit_key { return error(@MOD + '.' + @STRUCT + '.' + @FN + ' key `${sub_table}` has already been explicitly declared. Unexpected redeclaration at "${p.tok.kind}" "${p.tok.lit}" in this (excerpt): "...${p.excerpt()}..."') } if explicit_key.len == 1 || explicit_key == p.root_map_key { continue } // Check for "table injection": // https://github.com/BurntSushi/toml-test/blob/576db85/tests/invalid/table/injection-1.toml // https://github.com/BurntSushi/toml-test/blob/576db85/tests/invalid/table/injection-2.toml if p.build_abs_dotted_key(sub_table).starts_with(explicit_key) { return error(@MOD + '.' + @STRUCT + '.' + @FN + ' key `${dotted_key}` has already been explicitly declared. Unexpected redeclaration at "${p.tok.kind}" "${p.tok.lit}" in this (excerpt): "...${p.excerpt()}..."') } } // Register implicit declaration mut dotted_key_copy := dotted_key.clone() dotted_key_copy.pop() implicit_keys := todo_msvc_astring2dkey(dotted_key_copy) mut abs_dotted_key := p.build_abs_dotted_key(implicit_keys) if !p.implicit_declared.has(abs_dotted_key) { p.implicit_declared << abs_dotted_key } t := p.find_sub_table(sub_table)! unsafe { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'setting "${key}" = ${val} in table ${ptr_str(t)}') t[key.str()] = val } } else { p.ignore_while(parser.space_formatting) key, val := p.key_value()! t := p.find_table()! unsafe { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'setting "${key.str()}" = ${val} in table ${ptr_str(t)}') key_str := key.str() if _ := t[key_str] { return error(@MOD + '.' + @STRUCT + '.' + @FN + ' key "${key}" is already initialized with a value. At "${p.tok.kind}" "${p.tok.lit}" in this (excerpt): "...${p.excerpt()}..."') } t[key_str] = val } } p.peek_for_correct_line_ending_or_fail()! } .lsbr { p.check(.lsbr)! // '[' bracket mut peek_tok := p.peek_tok // Disallow `[ [table]]` if p.tok.kind in parser.space_formatting { peek_tok, _ = p.peek_over(1, parser.space_formatting)! if peek_tok.kind == .lsbr { return error(@MOD + '.' + @STRUCT + '.' + @FN + ' unexpected "${p.tok.kind}" "${p.tok.lit}" at this (excerpt): "...${p.excerpt()}..."') } } // Allow `[ d.e.f]` p.ignore_while(parser.space_formatting) // Peek forward as far as we can skipping over space formatting tokens. peek_tok, _ = p.peek_over(1, parser.keys_and_space_formatting)! if p.tok.kind == .lsbr { // Parse `[[table]]` unsafe { p.array_of_tables(mut &p.root_map)! } p.skip_next = true // skip calling p.next() in coming iteration util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'leaving double bracket at "${p.tok.kind}" "${p.tok.lit}". NEXT is "${p.peek_tok.kind} "${p.peek_tok.lit}"') } else if peek_tok.kind == .period { // Parse `[d.e.f]` dotted_key := p.dotted_key()! // So apparently TOML is a *very* key context sensitive language... // [[table]] <- parsed previously // ... // [table.key] <- parser is here // // `table.key` now shape shifts into being a *double array of tables* key... // ... but with a different set of rules - making it hard to reuse the code we already have for that ... // See `testdata/array_of_tables_edge_case__test.toml` for the type of constructs parsed. if p.last_aot.len == 1 && dotted_key.len > 1 && dotted_key[0] == p.last_aot.str() { // Disallow re-declaring the key p.check_explicitly_declared_array_of_tables(dotted_key)! p.check(.rsbr)! p.ignore_while(parser.space_formatting) arr := p.find_array_of_tables()! if val := arr[p.last_aot_index] { if val is map[string]ast.Value { mut m := map[string]ast.Value{} p.table_contents(mut m)! unsafe { mut mut_val := &val if dotted_key.len == 2 { // [table.key] mut_val[dotted_key[1].str()] = m } else { // [table.key.key.etc] mut dotted_key_copy := dotted_key.clone() dotted_key_copy.delete(0) new_key := todo_msvc_astring2dkey(dotted_key_copy) sub_table, key := p.sub_table_key(new_key) t := p.find_in_table(mut mut_val, sub_table)! util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'setting "${key}" = ${val} in table ${ptr_str(t)}') t[new_key.last().str()] = m } } } else { return error(@MOD + '.' + @STRUCT + '.' + @FN + ' "${p.last_aot_index}" in array is not a map but `${typeof(val).name}`') } } continue } // Disallow re-declaring the key p.check_explicitly_declared(dotted_key)! p.explicit_declared << dotted_key // ... also check implicitly declared keys p.check_implicitly_declared(dotted_key)! p.ignore_while(parser.space_formatting) util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'setting root map key to `${dotted_key}` at "${p.tok.kind}" "${p.tok.lit}"') p.root_map_key = dotted_key p.allocate_table(p.root_map_key)! p.expect(.rsbr)! p.peek_for_correct_line_ending_or_fail()! } else { // Parse `[key]` key := p.key()! dotted_key := DottedKey([key.str()]) // Disallow re-declaring the key p.check_explicitly_declared(dotted_key)! p.explicit_declared << dotted_key // Check for footgun redeclaration in this odd way: // [[tbl]] // [tbl] if p.last_aot == dotted_key { return error(@MOD + '.' + @STRUCT + '.' + @FN + ' key `${dotted_key}` has already been explicitly declared. Unexpected redeclaration at "${p.tok.kind}" "${p.tok.lit}" in this (excerpt): "...${p.excerpt()}..."') } // Allow [ key ] p.ignore_while(parser.space_formatting) util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'setting root map key to `${dotted_key}` at "${p.tok.kind}" "${p.tok.lit}"') p.root_map_key = dotted_key p.allocate_table(p.root_map_key)! p.next()! p.expect(.rsbr)! p.peek_for_correct_line_ending_or_fail()! } } .eof { return } else { return error(@MOD + '.' + @STRUCT + '.' + @FN + ' could not parse "${p.tok.kind}" "${p.tok.lit}" in this (excerpt): "...${p.excerpt()}..."') } } } } // excerpt returns a string of the characters surrounding `Parser.tok.pos` fn (p Parser) excerpt() string { return p.scanner.excerpt(p.tok.pos, 10) } // table_contents parses next tokens into a map of `ast.Value`s. // The V `map` type is corresponding to a "table" in TOML. pub fn (mut p Parser) table_contents(mut tbl map[string]ast.Value) ! { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'parsing table contents...') for p.tok.kind != .eof { if p.peek_tok.kind == .lsbr { return } if !p.skip_next { p.next()! } else { p.skip_next = false } util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'parsing token "${p.tok.kind}" "${p.tok.lit}"') match p.tok.kind { .hash { c := p.comment() p.ast_root.comments << c util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'skipping comment "${c.text}"') } .whitespace, .tab, .nl, .cr { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'skipping formatting "${p.tok.kind}" "${p.tok.lit}"') continue } .bare, .quoted, .number, .minus, .underscore { // Peek forward as far as we can skipping over space formatting tokens. peek_tok, _ := p.peek_over(1, parser.keys_and_space_formatting)! if peek_tok.kind == .period { dotted_key, val := p.dotted_key_value()! sub_table, key := p.sub_table_key(dotted_key) t := p.find_in_table(mut tbl, sub_table)! unsafe { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'setting "${key}" = ${val} in table ${ptr_str(t)}') t[key.str()] = val } } else { p.ignore_while(parser.space_formatting) key, val := p.key_value()! unsafe { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'setting "${key.str()}" = ${val} in table ${ptr_str(tbl)}') key_str := key.str() if _ := tbl[key_str] { return error(@MOD + '.' + @STRUCT + '.' + @FN + ' key "${key}" is already initialized with a value. At "${p.tok.kind}" "${p.tok.lit}" in this (excerpt): "...${p.excerpt()}..."') } tbl[key_str] = val } } p.peek_for_correct_line_ending_or_fail()! } .eof { break } else { return error(@MOD + '.' + @STRUCT + '.' + @FN + ' could not parse "${p.tok.kind}" "${p.tok.lit}" in this (excerpt): "...${p.excerpt()}..."') } } } } // inline_table parses next tokens into a map of `ast.Value`s. // The V map type is corresponding to a "table" in TOML. pub fn (mut p Parser) inline_table(mut tbl map[string]ast.Value) ! { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'parsing inline table into ${ptr_str(tbl)}...') mut previous_token_was_value := false for p.tok.kind != .eof { p.next()! util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'parsing token "${p.tok.kind}"') if previous_token_was_value { p.ignore_while(parser.space_formatting) if p.tok.kind != .rcbr { p.expect(.comma)! } previous_token_was_value = false } match p.tok.kind { .whitespace, .tab { /* if !p.scanner.config.tokenize_formatting { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'skipping "$p.tok.kind" "$p.tok.lit"') continue }*/ util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'skipping formatting "${p.tok.kind}" "${p.tok.lit}"') continue } .comma { p.ignore_while_peek(parser.space_formatting) if p.peek_tok.kind in [.comma, .rcbr] { p.next()! // Forward to the peek_tok return error(@MOD + '.' + @STRUCT + '.' + @FN + ' unexpected "${p.tok.kind}" "${p.tok.lit}" at this (excerpt): "...${p.excerpt()}..."') } util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'skipping comma table value separator "${p.tok.lit}"') continue } .rcbr { // '}' bracket return } .bare, .quoted, .number, .minus, .underscore { // Peek forward as far as we can skipping over space formatting tokens. peek_tok, _ := p.peek_over(1, parser.space_formatting)! if peek_tok.kind == .period { dotted_key, val := p.dotted_key_value()! sub_table, key := p.sub_table_key(dotted_key) mut t := p.find_in_table(mut tbl, sub_table)! unsafe { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'inserting @6 "${key}" = ${val} into ${ptr_str(t)}') t[key.str()] = val } } else { p.ignore_while(parser.space_formatting) key, val := p.key_value()! key_str := key.str() if _ := tbl[key_str] { return error(@MOD + '.' + @STRUCT + '.' + @FN + ' key "${key_str}" is already initialized with a value. At "${p.tok.kind}" "${p.tok.lit}" in this (excerpt): "...${p.excerpt()}..."') } util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'inserting @5 "${key_str}" = ${val} into ${ptr_str(tbl)}') tbl[key_str] = val } previous_token_was_value = true } else { return error(@MOD + '.' + @STRUCT + '.' + @FN + ' unexpected "${p.tok.kind}" "${p.tok.lit}" at this (excerpt): "...${p.excerpt()}..."') } } } // Make sure the inline-table actually use the return at .rcbr match branch. return error(@MOD + '.' + @STRUCT + '.' + @FN + ' unexpected end of inline-table "${p.tok.kind}" "${p.tok.lit}" at this (excerpt): "...${p.excerpt()}..."') } // array_of_tables parses next tokens into an array of `ast.Value`s. [autofree_bug; manualfree] pub fn (mut p Parser) array_of_tables(mut table map[string]ast.Value) ! { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'parsing array of tables "${p.tok.kind}" "${p.tok.lit}"') // NOTE this is starting to get ugly. TOML isn't simple at this point p.check(.lsbr)! // '[' bracket // Allow [[ key]] p.ignore_while(parser.space_formatting) peek_tok, _ := p.peek_over(1, parser.space_formatting)! p.ignore_while(parser.space_formatting) // [[key.key]] horror if peek_tok.kind == .period { p.double_array_of_tables(mut table)! return } key := p.key()! p.next()! // Allow [[key ]] p.ignore_while(parser.space_formatting) p.check(.rsbr)! p.peek_for_correct_line_ending_or_fail()! p.expect(.rsbr)! p.ignore_while(parser.all_formatting) dotted_key := DottedKey([key.str()]) dotted_key_str := dotted_key.str() // Disallow re-declaring the key p.check_explicitly_declared(dotted_key)! unsafe { if val := table[dotted_key_str] { if val is []ast.Value { arr := &(table[dotted_key_str] as []ast.Value) arr << p.array_of_tables_contents()! table[dotted_key_str] = arr } else { return error(@MOD + '.' + @STRUCT + '.' + @FN + ' table[${dotted_key_str}] is not an array. (excerpt): "...${p.excerpt()}..."') } } else { table[dotted_key_str] = p.array_of_tables_contents()! } } p.last_aot = dotted_key unsafe { arr := &(table[p.last_aot.str()] as []ast.Value) p.last_aot_index = arr.len - 1 } } // array_of_tables_contents parses next tokens into an array of `ast.Value`s. pub fn (mut p Parser) array_of_tables_contents() ![]ast.Value { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'parsing contents from "${p.tok.kind}" "${p.tok.lit}"') mut tbl := map[string]ast.Value{} p.table_contents(mut tbl)! mut arr := []ast.Value{} arr << tbl util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'parsed array of tables ${ast.Value(arr)}. leaving at "${p.tok.kind}" "${p.tok.lit}"') return arr } // double_array_of_tables parses next tokens into an array of tables of arrays of `ast.Value`s... [autofree_bug; manualfree] pub fn (mut p Parser) double_array_of_tables(mut table map[string]ast.Value) ! { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'parsing nested array of tables "${p.tok.kind}" "${p.tok.lit}"') dotted_key := p.dotted_key()! p.ignore_while(parser.space_formatting) p.check(.rsbr)! p.expect(.rsbr)! p.ignore_while(parser.all_formatting) if dotted_key.len != 2 { return error(@MOD + '.' + @STRUCT + '.' + @FN + ' nested array of tables does not support more than 2 levels. (excerpt): "...${p.excerpt()}..."') } p.check_explicitly_declared(dotted_key)! if !p.explicit_declared_array_of_tables.has(dotted_key) { p.explicit_declared_array_of_tables << dotted_key } first := DottedKey([dotted_key[0]]) // The array that holds the entries last := DottedKey([dotted_key[1]]) // The key the parsed array data should be added to mut t_arr := &[]ast.Value(0) mut t_map := ast.Value(ast.Null{}) unsafe { // NOTE this is starting to get EVEN uglier. TOML is not *at all* simple at this point... if first != p.last_aot { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, '${first} != ${p.last_aot}') // Implicit allocation if p.last_aot.len == 0 { p.last_aot = first mut nm := &p.root_map if first.str() in table.keys() { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'adding to existing table entry at `${first}`.') nm = &(table[first.str()] as map[string]ast.Value) } else { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'implicit allocation of map for `${first}` in dotted key `${dotted_key}`.') nm = &map[string]ast.Value{} // We register this implicit allocation as *explicit* to be able to catch // special cases like: // https://github.com/BurntSushi/toml-test/blob/576db852/tests/invalid/table/array-implicit.toml p.explicit_declared << first } nm[last.str()] = []ast.Value{} table[first.str()] = ast.Value(nm) t_arr = &(nm[last.str()] as []ast.Value) t_arr << p.array_of_tables_contents()! return } else { return error(@MOD + '.' + @STRUCT + '.' + @FN + ' nested array of tables key "${first}" does not match "${p.last_aot}". (excerpt): "...${p.excerpt()}..."') } } t_arr = &(table[p.last_aot.str()] as []ast.Value) t_map = ast.Value(map[string]ast.Value{}) if p.last_aot_index < t_arr.len { t_map = t_arr[p.last_aot_index] } mut t := &(t_map as map[string]ast.Value) if val := t[last.str()] { if val is []ast.Value { arr := &(val as []ast.Value) arr << p.double_array_of_tables_contents(dotted_key)! t[last.str()] = arr } else { return error(@MOD + '.' + @STRUCT + '.' + @FN + ' t[${last.str()}] is not an array. (excerpt): "...${p.excerpt()}..."') } } else { t[last.str()] = p.double_array_of_tables_contents(dotted_key)! } if t_arr.len == 0 { t_arr << t p.last_aot_index = t_arr.len - 1 } } } // double_array_of_tables_contents parses next tokens into an array of `ast.Value`s. pub fn (mut p Parser) double_array_of_tables_contents(target_key DottedKey) ![]ast.Value { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'parsing contents from "${p.tok.kind}" "${p.tok.lit}"') mut tbl := map[string]ast.Value{} mut implicit_allocation_key := DottedKey([]string{}) mut peeked_over := 0 mut peek_tok := p.peek_tok for p.tok.kind != .eof { p.next()! util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'parsing token "${p.tok.kind}"') p.ignore_while(parser.all_formatting) // Peek forward as far as we can skipping over space formatting tokens. peek_tok, peeked_over = p.peek_over(1, parser.space_formatting)! // Peek for occurrence of `[[` if peek_tok.kind == .lsbr { peek_tok, peeked_over = p.peek_over(peeked_over + 1, parser.space_formatting)! if peek_tok.kind == .lsbr { mut arr := []ast.Value{} arr << tbl return arr } } match p.tok.kind { .bare, .quoted, .number, .minus, .underscore { // Peek forward as far as we can skipping over space formatting tokens. peek_tok, _ = p.peek_over(1, parser.space_formatting)! if peek_tok.kind == .period { mut dotted_key, val := p.dotted_key_value()! if implicit_allocation_key.len > 0 { dotted_key.insert(0, implicit_allocation_key) } sub_table, key := p.sub_table_key(dotted_key) mut t := p.find_in_table(mut tbl, sub_table)! unsafe { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'inserting @6 "${key}" = ${val} into ${ptr_str(t)}') t[key.str()] = val } } else { key, val := p.key_value()! mut t := unsafe { &tbl } if implicit_allocation_key.len > 0 { t = p.find_in_table(mut tbl, implicit_allocation_key)! } unsafe { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'inserting @7 "${key}" = ${val} into ${ptr_str(t)}') t[key.str()] = val } } } .lsbr { p.check(.lsbr)! // '[' bracket peek_tok = p.peek_tok // Allow `[ d.e.f]` p.ignore_while(parser.space_formatting) // Peek forward as far as we can skipping over space formatting tokens. peek_tok, _ = p.peek_over(1, parser.space_formatting)! if peek_tok.kind == .period { // Parse `[d.e.f]` p.ignore_while(parser.space_formatting) dotted_key := p.dotted_key()! implicit_allocation_key = dotted_key if dotted_key.len > 2 { implicit_allocation_key = dotted_key[2..] } p.ignore_while(parser.space_formatting) util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'keys are: dotted `${dotted_key}`, target `${target_key}`, implicit `${implicit_allocation_key}` at "${p.tok.kind}" "${p.tok.lit}"') p.expect(.rsbr)! p.peek_for_correct_line_ending_or_fail()! p.explicit_declared << dotted_key continue } else { return error(@MOD + '.' + @STRUCT + '.' + @FN + ' could not parse "${p.tok.kind}" "${p.tok.lit}" in this (excerpt): "...${p.excerpt()}..."') } } else { break } } } mut arr := []ast.Value{} arr << tbl util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'parsed array of tables ${ast.Value(arr)}. leaving at "${p.tok.kind}" "${p.tok.lit}"') return arr } // array parses next tokens into an array of `ast.Value`s. pub fn (mut p Parser) array() ![]ast.Value { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'parsing array...') mut arr := []ast.Value{} p.expect(.lsbr)! // '[' bracket mut previous_token_was_value := false for p.tok.kind != .eof { p.next()! util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'parsing token "${p.tok.kind}" "${p.tok.lit}"') if previous_token_was_value { p.ignore_while(parser.all_formatting) if p.tok.kind != .rsbr && p.tok.kind != .hash { p.expect(.comma)! } previous_token_was_value = false } match p.tok.kind { .boolean { arr << ast.Value(p.boolean()!) previous_token_was_value = true } .comma { p.ignore_while_peek(parser.space_formatting) // Trailing commas before array close is allowed // so we do not do `if p.peek_tok.kind == .rsbr { ... }` // Check for known errors: if p.peek_tok.kind in [.comma, .bare] { p.next()! // Forward to the peek_tok return error(@MOD + '.' + @STRUCT + '.' + @FN + ' unexpected "${p.tok.kind}" "${p.tok.lit}" at this (excerpt): "...${p.excerpt()}..."') } util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'skipping comma table value separator "${p.tok.lit}"') continue } .eof { return error(@MOD + '.' + @STRUCT + '.' + @FN + ' could not parse array. Reached EOF "${p.tok.kind}" "${p.tok.lit}" ("${p.tok.lit}") in this (excerpt): "...${p.excerpt()}..."') } .hash { c := p.comment() p.ast_root.comments << c util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'skipping comment "${c.text}"') } .lcbr { p.ignore_while(parser.space_formatting) mut t := map[string]ast.Value{} p.inline_table(mut t)! arr << ast.Value(t) previous_token_was_value = true } .number { val := p.number_or_date()! arr << val previous_token_was_value = true } .quoted { arr << ast.Value(p.quoted()) previous_token_was_value = true } .lsbr { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'parsing array in array "${p.tok.kind}" "${p.tok.lit}"') arr << ast.Value(p.array()!) previous_token_was_value = true } .rsbr { break } else { error(@MOD + '.' + @STRUCT + '.' + @FN + ' could not parse "${p.tok.kind}" "${p.tok.lit}" ("${p.tok.lit}") in this (excerpt): "...${p.excerpt()}..."') } } } p.expect(.rsbr)! // ']' bracket $if debug { flat := arr.str().replace('\n', r'\n') util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'parsed array: ${flat} . Currently @ token "${p.tok.kind}"') } return arr } // comment returns an `ast.Comment` type. pub fn (mut p Parser) comment() ast.Comment { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'parsed hash comment "#${p.tok.lit}"') return ast.Comment{ text: p.tok.lit pos: p.tok.pos() } } // key parse and returns an `ast.Key` type. // Keys are the token(s) appearing before an assignment operator (=). pub fn (mut p Parser) key() !ast.Key { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'parsing key from "${p.tok.lit}" ...') mut key := ast.Key(ast.Null{}) if p.tok.kind == .number { if p.peek_tok.kind == .minus { mut lits := p.tok.lit pos := p.tok.pos() for p.peek_tok.kind != .assign && p.peek_tok.kind != .period && p.peek_tok.kind != .rsbr { p.next()! if p.tok.kind !in parser.space_formatting { lits += p.tok.lit } } return ast.Key(ast.Bare{ text: lits pos: pos }) } key = ast.Key(p.number()) } else { key = match p.tok.kind { .bare, .underscore, .minus { ast.Key(p.bare()!) } .boolean { ast.Key(p.boolean()!) } .quoted { ast.Key(p.quoted()) } else { ast.Key(ast.Null{}) } } } // NOTE kept for eased debugging // util.printdbg(@MOD +'.' + @STRUCT + '.' + @FN, 'parsed key "$p.tok.lit"') // panic(@MOD + '.' + @STRUCT + '.' + @FN + ' could not parse ${p.tok.kind} ("${p.tok.lit}") token \n$p.tok') // return ast.Key(ast.Bare{}) if key is ast.Null { return error(@MOD + '.' + @STRUCT + '.' + @FN + ' key expected .bare, .underscore, .number, .quoted or .boolean but got "${p.tok.kind}"') } // A few small exceptions that can't easily be done via `checker` or `decoder` *after* the // main table has been build since information like `is_multiline` is lost when using the key.text as a // V `map` key directly. if key is ast.Quoted { if p.config.run_checks { quoted := key as ast.Quoted if quoted.is_multiline { return error(@MOD + '.' + @STRUCT + '.' + @FN + ' multiline string as key is not allowed. (excerpt): "...${p.excerpt()}..."') } chckr := checker.Checker{ scanner: p.scanner } chckr.check_quoted(quoted)! } if p.config.decode_values { mut quoted := key as ast.Quoted decoder.decode_quoted_escapes(mut quoted)! key = ast.Key(quoted) } } return key } // key_value parse and returns a pair `ast.Key` and `ast.Value` type. // see also `key()` and `value()` pub fn (mut p Parser) key_value() !(ast.Key, ast.Value) { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'parsing key value pair...') key := p.key()! p.next()! p.ignore_while(parser.space_formatting) p.check(.assign)! // Assignment operator p.ignore_while(parser.space_formatting) value := p.value()! util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'parsed key value pair. `${key} = ${value}`') p.explicit_declared << p.build_abs_dotted_key(DottedKey([ key.str(), ])) return key, value } // dotted_key_value parse and returns a pair `DottedKey` and `ast.Value` type. // see also `key()` and `value()` pub fn (mut p Parser) dotted_key_value() !(DottedKey, ast.Value) { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'parsing dotted key value pair...') p.ignore_while(parser.space_formatting) dotted_key := p.dotted_key()! p.ignore_while(parser.space_formatting) p.check(.assign)! p.ignore_while(parser.space_formatting) value := p.value()! util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'parsed dotted key value pair `${dotted_key} = ${value}`...') p.explicit_declared << p.build_abs_dotted_key(dotted_key) return dotted_key, value } // value parse and returns an `ast.Value` type. // values are the token(s) appearing after an assignment operator (=). pub fn (mut p Parser) value() !ast.Value { util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'parsing value from token "${p.tok.kind}" "${p.tok.lit}"...') mut value := ast.Value(ast.Null{}) if p.tok.kind == .number { number_or_date := p.number_or_date()! value = number_or_date } else { value = match p.tok.kind { .quoted { ast.Value(p.quoted()) } .boolean { ast.Value(p.boolean()!) } .lsbr { ast.Value(p.array()!) } .lcbr { p.ignore_while(parser.space_formatting) mut t := map[string]ast.Value{} p.inline_table(mut t)! ast.Value(t) } else { ast.Value(ast.Null{}) } } if value is ast.Null { return error(@MOD + '.' + @STRUCT + '.' + @FN + ' value expected .boolean, .quoted, .lsbr, .lcbr or .number got "${p.tok.kind}" "${p.tok.lit}" in this (excerpt): "...${p.excerpt()}..."') } } util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'parsed "${p.tok.kind}" as value ${value}') return value } // number_or_date parse and returns an `ast.Value` type as // one of [`ast.Date`, `ast.Time`, `ast.DateTime`, `ast.Number`] pub fn (mut p Parser) number_or_date() !ast.Value { // Handle Date/Time if p.peek_tok.kind == .minus || p.peek_tok.kind == .colon { date_time_type := p.date_time()! match date_time_type { ast.Date { return ast.Value(date_time_type as ast.Date) } ast.Time { return ast.Value(date_time_type as ast.Time) } ast.DateTime { return ast.Value(date_time_type as ast.DateTime) } } } return ast.Value(p.number()) } // bare parse and returns an `ast.Bare` type. pub fn (mut p Parser) bare() !ast.Bare { mut lits := p.tok.lit pos := p.tok.pos() for p.peek_tok.kind != .assign && p.peek_tok.kind != .period && p.peek_tok.kind != .rsbr && p.peek_tok.kind !in parser.space_formatting { p.next()! if p.tok.kind == .bare || p.tok.kind == .minus || p.tok.kind == .underscore { lits += p.tok.lit continue } return error(@MOD + '.' + @STRUCT + '.' + @FN + ' bare key expected .bare, .minus, or .underscore but got "${p.tok.kind}"') } return ast.Bare{ text: lits pos: pos } } // quoted parse and returns an `ast.Quoted` type. pub fn (mut p Parser) quoted() ast.Quoted { // To get more info about the quote type and enable better checking, // the scanner is returning the literal *with* single- or double-quotes. mut quote := p.tok.lit[0] is_multiline := p.tok.lit.len >= 6 && p.tok.lit[1] == quote && p.tok.lit[2] == quote mut lit := p.tok.lit[1..p.tok.lit.len - 1] if is_multiline { lit = p.tok.lit[3..p.tok.lit.len - 3] // From https://toml.io/en/v1.0.0#string // "Multi-line literal strings [...] A newline immediately following the opening // delimiter will be trimmed. All other content between the delimiters // is interpreted as-is without modification." if lit.len > 0 && lit[0] == `\n` { lit = lit[1..] } } return ast.Quoted{ text: lit pos: p.tok.pos() quote: quote is_multiline: is_multiline } } // boolean parse and returns an `ast.Bool` type. pub fn (mut p Parser) boolean() !ast.Bool { if p.tok.lit !in ['true', 'false'] { return error(@MOD + '.' + @STRUCT + '.' + @FN + ' expected literal to be either `true` or `false` got "${p.tok.kind}"') } return ast.Bool{ text: p.tok.lit pos: p.tok.pos() } } // number parse and returns an `ast.Number` type. pub fn (mut p Parser) number() ast.Number { return ast.Number{ text: p.tok.lit pos: p.tok.pos() } } // date_time parses dates and time in RFC 3339 format. // https://datatracker.ietf.org/doc/html/rfc3339 pub fn (mut p Parser) date_time() !ast.DateTimeType { // Date and/or Time mut lit := '' pos := p.tok.pos() mut date := ast.Date{} mut time := ast.Time{} if p.peek_tok.kind == .minus { date = p.date()! lit += date.text // Look for any THH:MM:SS or HH:MM:SS if (p.peek_tok.kind == .bare && (p.peek_tok.lit.starts_with('T') || p.peek_tok.lit.starts_with('t'))) || p.peek_tok.kind == .whitespace { p.next()! // Advance to token with Txx or whitespace special case if p.tok.lit.starts_with('T') || p.tok.lit.starts_with('t') { lit += p.tok.lit[0].ascii_str() //'T' or 't' } else { lit += p.tok.lit p.next()! } time = p.time()! lit += time.text util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'parsed date-time: "${lit}"') return ast.DateTime{ text: lit pos: pos date: date time: time } } } else if p.peek_tok.kind == .colon { time = p.time()! return time } return ast.Date{ text: lit pos: pos } } // date parse and returns an `ast.Date` type. pub fn (mut p Parser) date() !ast.Date { // Date mut lit := p.tok.lit pos := p.tok.pos() p.check(.number)! lit += p.tok.lit p.check(.minus)! lit += p.tok.lit p.check(.number)! lit += p.tok.lit p.check(.minus)! lit += p.tok.lit p.expect(.number)! util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'parsed date: "${lit}"') return ast.Date{ text: lit pos: pos } } // time parse and returns an `ast.Time` type. pub fn (mut p Parser) time() !ast.Time { // Time mut lit := p.tok.lit pos := p.tok.pos() if p.is_at(.bare) && (lit.starts_with('T') || lit.starts_with('t')) { if p.tok.lit.starts_with('T') { lit = lit.all_after('T') } else if p.tok.lit.starts_with('t') { lit = lit.all_after('t') } p.next()! } else { p.check(.number)! } lit += p.tok.lit p.check(.colon)! lit += p.tok.lit p.check(.number)! lit += p.tok.lit // TODO does TOML even have optional seconds? // if p.peek_tok.kind == .colon { p.check(.colon)! lit += p.tok.lit p.expect(.number)! //} // Optional milliseconds if p.peek_tok.kind == .period { p.next()! lit += p.tok.lit // lit += '.' p.check(.period)! lit += p.tok.lit p.expect(.number)! } // Parse offset if p.peek_tok.kind == .minus || p.peek_tok.kind == .plus { p.next()! lit += p.tok.lit // lit += '-' p.check_one_of([.minus, .plus])! lit += p.tok.lit p.check(.number)! lit += p.tok.lit p.check(.colon)! lit += p.tok.lit p.expect(.number)! } else if p.peek_tok.kind == .bare && (p.peek_tok.lit == 'Z' || p.peek_tok.lit == 'z') { p.next()! lit += p.tok.lit p.expect(.bare)! } util.printdbg(@MOD + '.' + @STRUCT + '.' + @FN, 'parsed time: "${lit}"') return ast.Time{ text: lit pos: pos } } // eof returns an `ast.EOF` type. pub fn (mut p Parser) eof() ast.EOF { return ast.EOF{ pos: p.tok.pos() } }