diff --git a/ctraits.vpnci b/ctraits.vpnci new file mode 100644 index 0000000..f8656e2 --- /dev/null +++ b/ctraits.vpnci @@ -0,0 +1,20 @@ +"""ctraits — Compile-time type trait library + +Provides compile-time type predicates with no runtime code generated. +All functions are evaluated exclusively at compile time and can be combined with the conditional +compilation statements cif / celif / celse. +""" +import t + +def isptr(x: t.CType) -> bool: + """Compile-time predicate to determine whether the type of x is a pointer. + + Returns True if x is a pointer type (ptr_count > 0); otherwise returns False. + Can be used alongside cif to implement conditional compilation. + + Example: + cif ctraits.isptr(my_var): + # This branch compiles only when my_var is a pointer type + ... + """ + pass \ No newline at end of file diff --git a/tomllib/__init__.vp b/tomllib/__init__.vp new file mode 100644 index 0000000..5cb20d4 --- /dev/null +++ b/tomllib/__init__.vp @@ -0,0 +1,20 @@ +# tomllib - TOML parser producing a json.JsonValue tree. +# +# TOML and JSON share the same structural model (object / array / scalar), so a document parses +# directly into json.JsonValue and can be re-emitted with json.write. See __parser.vp for the +# supported subset and for the constructs that are deliberately rejected (date-time, inf/nan). +# +# Error convention (identical to json.parse): +# - a JSON_NULL node => the source is malformed or uses a rejected construct; +# - None => the memory pool is exhausted. +# Callers must test `doc == None or doc.vtype == json.JSON_NULL` before walking the tree. +# +# Usage: +# import tomllib +# import json +# doc: json.JsonValue* = tomllib.parse(pool, text) +# if doc == None or doc.vtype == json.JSON_NULL: return -1 # 解析失败 +# name: json.JsonValue* = doc["project"]["name"] +# if name != None: stdio.printf("name=%s\n", name.str_val) + +from .__parser import parse diff --git a/tomllib/__parser.vp b/tomllib/__parser.vp new file mode 100644 index 0000000..54f4718 --- /dev/null +++ b/tomllib/__parser.vp @@ -0,0 +1,703 @@ +import memhub +import string +from json import ( + JsonValue, JSON_NULL, JSON_BOOL, JSON_INT, JSON_FLOAT, + JSON_STRING, JSON_ARRAY, JSON_OBJECT, + null, bool_val, int_val, float_val, string_val, array, object, + object_set, +) + + +# ============================================================ +# tomllib - TOML parser producing a json.JsonValue tree. +# +# The JSON value model is structurally the same as TOML's (object/array/scalar), so TOML parses +# straight into JsonValue; writing it back out is json.write's job. +# +# Supported: [table] / [[array of tables]] headers, dotted keys (a.b.c = 1), inline tables, +# arrays (multi-line, with comments), basic + literal strings (single and triple quoted, with +# escapes and line-ending backslash), integers (decimal/0x/0o/0b, underscores, sign), floats +# (fraction/exponent), booleans, comments, blank lines. +# +# Deliberately rejected (TOML constructs with no json.JsonValue counterpart - never silently +# downgraded): date-time values (1979-05-27, 07:32:00, with Z/offset), and inf/nan. +# +# Return convention (same as json.parse): parse() returns a JSON_NULL node when the source is +# malformed or uses a rejected construct, and None only when the pool is exhausted. Callers must +# test `v == None or v.vtype == JSON_NULL` before walking the tree. +# ============================================================ + +# Maximum depth of a dotted key / table header path (stack-allocated segment array). +define TOML_MAX_KEY_SEGMENTS = 32 +# Explicitly-defined table path registry capacity (bytes); overflow is a parse failure. +define TOML_DEFS_CAP = 65536 + + +# TomlState - parser cursor plus the mpool all nodes are allocated from. +class TomlState: + src: str + pos: sizet = 0 + len: sizet = 0 + pool: memhub.MemManager* + root: JsonValue* = None + cur: JsonValue* = None + cur_path: str = None + defs: str = None + defs_len: sizet = 0 + # 1 while the current section is an element of an [[array of tables]]: the registry below is + # keyed on the dotted path only, so the same dotted key in two different elements of the same + # array would otherwise look like a duplicate definition. + in_array: int = 0 + + def __init__(self, src: str, pool: memhub.MemManager*): + self.src = src + self.len = string.strlen(src) if src != None else 0 + self.pool = pool + + +# ============================================================ +# Character helpers +# ============================================================ + +def _peek(s: TomlState*) -> char: + if s.pos >= s.len: return '\0' + return s.src[s.pos] + +def _advance(s: TomlState*) -> char: + if s.pos >= s.len: return '\0' + ch: char = s.src[s.pos] + s.pos += 1 + return ch + +def _is_space(ch: char) -> bool: + return ch == ' ' or ch == '\t' + +def _is_newline(ch: char) -> bool: + return ch == '\n' or ch == '\r' + +def _is_digit(ch: char) -> bool: + return ch >= '0' and ch <= '9' + +def _hex_val(ch: char) -> int: + if _is_digit(ch): return int(ch) - 48 + if ch >= 'a' and ch <= 'f': return int(ch) - 87 + if ch >= 'A' and ch <= 'F': return int(ch) - 55 + return -1 + +def _is_bare_key_char(ch: char) -> bool: + if ch >= 'a' and ch <= 'z': return True + if ch >= 'A' and ch <= 'Z': return True + if _is_digit(ch): return True + return ch == '_' or ch == '-' + +def _skip_spaces(s: TomlState*): + while s.pos < s.len and _is_space(s.src[s.pos]): s.pos += 1 + +def _skip_to_line_end(s: TomlState*): + while s.pos < s.len and not _is_newline(s.src[s.pos]): s.pos += 1 + +# Spaces, newlines and # comments: the filler between array items and between top-level lines. +def _skip_filler(s: TomlState*): + while s.pos < s.len: + ch: char = s.src[s.pos] + if _is_space(ch) or _is_newline(ch): s.pos += 1 + elif ch == '#': _skip_to_line_end(s) + else: break + +# Trailing part of a completed statement: spaces, an optional comment, then the line must end. +def _skip_line_tail(s: TomlState*) -> int: + _skip_spaces(s) + if s.pos < s.len and s.src[s.pos] == '#': _skip_to_line_end(s) + if s.pos >= s.len: return 0 + if _is_newline(s.src[s.pos]): s.pos += 1; return 0 + return -1 + +# Match a whole keyword (true/false) at the cursor without consuming it. +def _match_word(s: TomlState*, w: str) -> int: + n: sizet = string.strlen(w) + if s.pos + n > s.len: return 0 + if string.strncmp(s.src + s.pos, w, n) == 0: return 1 + return 0 + +# "..." / ''' open a multi-line string. +def _is_multiline_open(s: TomlState*, q: char) -> int: + if s.pos + 2 < s.len and s.src[s.pos + 1] == q and s.src[s.pos + 2] == q: return 1 + return 0 + + +# ============================================================ +# Small allocation helpers +# ============================================================ + +def _copy_span(pool: memhub.MemManager*, src: str, start: sizet, n: sizet) -> str: + buf: str = pool.alloc(n + 1) + if buf == None: return None + i: sizet = 0 + while i < n: + buf[i] = src[start + i] + i += 1 + buf[n] = '\0' + return buf + +# Encode one code point as UTF-8 (used by \uXXXX / \UXXXXXXXX); returns the new write index. +def _utf8_put(buf: str, di: sizet, cp: int) -> sizet: + if cp < 0: cp = 0xFFFD + if cp < 0x80: + buf[di] = char(cp) + return di + 1 + if cp < 0x800: + buf[di] = char(0xC0 | (cp >> 6)) + buf[di + 1] = char(0x80 | (cp & 0x3F)) + return di + 2 + if cp < 0x10000: + buf[di] = char(0xE0 | (cp >> 12)) + buf[di + 1] = char(0x80 | ((cp >> 6) & 0x3F)) + buf[di + 2] = char(0x80 | (cp & 0x3F)) + return di + 3 + buf[di] = char(0xF0 | (cp >> 18)) + buf[di + 1] = char(0x80 | ((cp >> 12) & 0x3F)) + buf[di + 2] = char(0x80 | ((cp >> 6) & 0x3F)) + buf[di + 3] = char(0x80 | (cp & 0x3F)) + return di + 4 + +# Read n hex digits that follow a \u / \U escape at s.pos; -1 on malformed input. +def _read_hex_escape(s: TomlState*, buf: str, di: sizet, n: sizet) -> int: + cp: int = 0 + i: sizet = 0 + while i < n: + p: sizet = s.pos + 1 + i + if p >= s.len: return -1 + v: int = _hex_val(s.src[p]) + if v < 0: return -1 + cp = (cp << 4) | v + i += 1 + s.pos = s.pos + n + return int(_utf8_put(buf, di, cp)) + + +# ============================================================ +# Strings +# ============================================================ + +def _parse_string_escape(s: TomlState*, buf: str, di: sizet) -> int: + """Decode the escape sequence after a backslash; returns the new write index, -1 on error.""" + if s.pos >= s.len: return -1 + esc: char = s.src[s.pos] + if esc == 'b': buf[di] = char(8); return int(di) + 1 + if esc == 't': buf[di] = char(9); return int(di) + 1 + if esc == 'n': buf[di] = char(10); return int(di) + 1 + if esc == 'f': buf[di] = char(12); return int(di) + 1 + if esc == 'r': buf[di] = char(13); return int(di) + 1 + if esc == '"': buf[di] = '"'; return int(di) + 1 + if esc == '\\': buf[di] = '\\'; return int(di) + 1 + if esc == 'u': return _read_hex_escape(s, buf, di, 4) + if esc == 'U': return _read_hex_escape(s, buf, di, 8) + return -1 + +# Basic string "...": escapes are decoded. The output never exceeds the remaining source length +# (\uXXXX is 6 source chars for at most 4 output bytes), so a remaining+1 buffer always fits. +def _parse_basic_string(s: TomlState*) -> str: + if _peek(s) != '"': return None + _advance(s) + start: sizet = s.pos + buf: str = s.pool.alloc(s.len - start + 1) + if buf == None: return None + di: sizet = 0 + while s.pos < s.len: + ch: char = s.src[s.pos] + if ch == '"': + s.pos += 1 + buf[di] = '\0' + return buf + if ch == '\\': + s.pos += 1 + nd: int = _parse_string_escape(s, buf, di) + if nd < 0: return None + di = nd + s.pos += 1 + continue + if _is_newline(ch): return None + buf[di] = ch + di += 1 + s.pos += 1 + return None + +# Literal string '...': taken verbatim, no escapes. +def _parse_literal_string(s: TomlState*) -> str: + if _peek(s) != "'": return None + _advance(s) + start: sizet = s.pos + while s.pos < s.len and s.src[s.pos] != "'": + if _is_newline(s.src[s.pos]): return None + s.pos += 1 + if s.pos >= s.len: return None + n: sizet = s.pos - start + s.pos += 1 + return _copy_span(s.pool, s.src, start, n) + +# Multi-line basic string """...""": the first newline is trimmed; \ at end of line trims all +# following whitespace/newlines; escapes are decoded. +def _parse_multiline_basic(s: TomlState*) -> str: + s.pos += 3 + if _peek(s) == '\r': s.pos += 1 + if _peek(s) == '\n': s.pos += 1 + start: sizet = s.pos + buf: str = s.pool.alloc(s.len - start + 1) + if buf == None: return None + di: sizet = 0 + while s.pos < s.len: + ch: char = s.src[s.pos] + if ch == '"' and s.pos + 2 < s.len and s.src[s.pos + 1] == '"' and s.src[s.pos + 2] == '"': + s.pos += 3 + buf[di] = '\0' + return buf + if ch == '\\': + p: sizet = s.pos + 1 + while p < s.len and _is_space(s.src[p]): p += 1 + if p < s.len and _is_newline(s.src[p]): + s.pos = p + 1 + while s.pos < s.len and (_is_space(s.src[s.pos]) or _is_newline(s.src[s.pos])): s.pos += 1 + continue + s.pos += 1 + nd: int = _parse_string_escape(s, buf, di) + if nd < 0: return None + di = nd + s.pos += 1 + continue + buf[di] = ch + di += 1 + s.pos += 1 + return None + +# Multi-line literal string '''...''': verbatim except the trimmed leading newline. +def _parse_multiline_literal(s: TomlState*) -> str: + s.pos += 3 + if _peek(s) == '\r': s.pos += 1 + if _peek(s) == '\n': s.pos += 1 + start: sizet = s.pos + while s.pos < s.len: + if s.src[s.pos] == "'" and s.pos + 2 < s.len and s.src[s.pos + 1] == "'" and s.src[s.pos + 2] == "'": + n: sizet = s.pos - start + s.pos += 3 + return _copy_span(s.pool, s.src, start, n) + s.pos += 1 + return None + + +# ============================================================ +# Numbers +# ============================================================ + +# Classify a raw number token. Date-time and inf/nan are rejected (no JsonValue counterpart). +def _classify_number(pool: memhub.MemManager*, tok: str, n: sizet) -> JsonValue*: + i: sizet = 0 + neg: int = 0 + if tok[0] == '-': neg = 1; i = 1 + elif tok[0] == '+': i = 1 + if i >= n: return None + # No JSON counterpart: date/time (':' or 'Z'), and inf/nan. + if tok[i] == 'i' or tok[i] == 'n': return None + j: sizet = i + while j < n: + ch: char = tok[j] + if ch == ':' or ch == 'T' or ch == 'Z': return None + # A '-'/'+' may only follow an exponent marker: "1e-3" is a float, "2024-05-27" is a date + # (rejected: json.JsonValue has no date-time kind). + if ch == '-' or ch == '+': + if tok[j - 1] != 'e' and tok[j - 1] != 'E': return None + j += 1 + # 0x / 0o / 0b integers + if i + 1 < n and tok[i] == '0': + base_ch: char = tok[i + 1] + if base_ch == 'x' or base_ch == 'o' or base_ch == 'b': + base: int = 16 + if base_ch == 'o': base = 8 + elif base_ch == 'b': base = 2 + acc: int64 = 0 + k: sizet = i + 2 + digits: sizet = 0 + while k < n: + ch2: char = tok[k] + if ch2 != '_': + v: int = _hex_val(ch2) + if v < 0 or v >= base: return None + acc = acc * base + v + digits += 1 + k += 1 + if digits == 0: return None + if neg != 0: acc = -acc + return int_val(pool, acc) + # decimal integer or float: strip '_', keep [0-9.eE+-] + clean: str = pool.alloc(n - i + 1) + if clean == None: return None + di: sizet = 0 + is_float: int = 0 + dots: int = 0 + exps: int = 0 + k2: sizet = i + while k2 < n: + ch3: char = tok[k2] + if ch3 == '_': k2 += 1; continue + if ch3 == '.': + dots += 1 + if dots > 1: return None # at most one fraction separator + is_float = 1 + elif ch3 == 'e' or ch3 == 'E': + exps += 1 + if exps > 1: return None # at most one exponent + is_float = 1 + elif not _is_digit(ch3) and ch3 != '+' and ch3 != '-': return None + clean[di] = ch3 + di += 1 + k2 += 1 + if di == 0: return None + clean[di] = '\0' + # Leading zeros: only a lone "0" may start the integer part, so "01" and "00.5" are rejected + # while "0.5" and "0e0" (integer part "0") are fine. + ip: sizet = 0 + while ip < di and _is_digit(clean[ip]): ip += 1 + if ip > 1 and clean[0] == '0': return None + if is_float != 0: + tailc: char = clean[di - 1] + if tailc == '.' or tailc == 'e' or tailc == 'E' or tailc == '+' or tailc == '-': return None + fv: double = string.atof(clean) + if neg != 0: fv = 0.0 - fv + return float_val(pool, fv) + iv: int64 = string.atoll(clean) + if neg != 0: iv = -iv + return int_val(pool, iv) + +# Number token: run to the next delimiter, then classify. +def _parse_number_value(s: TomlState*) -> JsonValue*: + start: sizet = s.pos + while s.pos < s.len: + ch: char = s.src[s.pos] + if _is_space(ch) or _is_newline(ch): break + if ch == ',' or ch == ']' or ch == '}' or ch == '#': break + s.pos += 1 + n: sizet = s.pos - start + if n == 0: return None + tok: str = _copy_span(s.pool, s.src, start, n) + if tok == None: return None + return _classify_number(s.pool, tok, n) + + +# ============================================================ +# Containers and values +# ============================================================ + +def _parse_array(s: TomlState*) -> JsonValue*: + _advance(s) # '[' + arr: JsonValue* = array(s.pool) + if arr == None: return None + while True: + _skip_filler(s) + if s.pos >= s.len: return None + if _peek(s) == ']': _advance(s); return arr + item: JsonValue* = _parse_value(s) + if item == None: return None + arr._append_child(item) + _skip_filler(s) + if s.pos < s.len and _peek(s) == ',': + _advance(s) + continue + if s.pos < s.len and _peek(s) == ']': _advance(s); return arr + return None + +def _parse_inline_table(s: TomlState*) -> JsonValue*: + _advance(s) # '{' + obj: JsonValue* = object(s.pool) + if obj == None: return None + _skip_spaces(s) + if s.pos < s.len and _peek(s) == '}': _advance(s); return obj + segs: [TOML_MAX_KEY_SEGMENTS]str + while True: + n: int = _parse_key_path(s, segs, TOML_MAX_KEY_SEGMENTS) + if n <= 0: return None + _skip_spaces(s) + if _peek(s) != '=': return None + _advance(s) + val: JsonValue* = _parse_value(s) + if val == None: return None + tbl: JsonValue* = _dotted_target(s, obj, segs, n, 0) + if tbl == None: return None + last: str = segs[n - 1] + if tbl[last] != None: return None + object_set(s.pool, tbl, last, val) + if tbl[last] == None: return None + _skip_spaces(s) + if s.pos < s.len and _peek(s) == '}': _advance(s); return obj + if s.pos < s.len and _peek(s) == ',': _advance(s); _skip_spaces(s); continue + return None + +def _parse_value(s: TomlState*) -> JsonValue*: + _skip_spaces(s) + if s.pos >= s.len: return None + ch: char = _peek(s) + if ch == '"': + sv: str = None + if _is_multiline_open(s, '"') != 0: sv = _parse_multiline_basic(s) + else: sv = _parse_basic_string(s) + if sv == None: return None + return string_val(s.pool, sv) + if ch == "'": + sv2: str = None + if _is_multiline_open(s, "'") != 0: sv2 = _parse_multiline_literal(s) + else: sv2 = _parse_literal_string(s) + if sv2 == None: return None + return string_val(s.pool, sv2) + if ch == '[': return _parse_array(s) + if ch == '{': return _parse_inline_table(s) + if ch == 't': + if _match_word(s, "true") != 0: s.pos += 4; return bool_val(s.pool, True) + return None + if ch == 'f': + if _match_word(s, "false") != 0: s.pos += 5; return bool_val(s.pool, False) + return None + if ch == '+' or ch == '-' or _is_digit(ch): return _parse_number_value(s) + return None + + +# ============================================================ +# Keys and table paths +# ============================================================ + +def _parse_key_segment(s: TomlState*) -> str: + _skip_spaces(s) + ch: char = _peek(s) + if ch == '"': + if _is_multiline_open(s, '"') != 0: return None + return _parse_basic_string(s) + if ch == "'": + if _is_multiline_open(s, "'") != 0: return None + return _parse_literal_string(s) + if not _is_bare_key_char(ch): return None + start: sizet = s.pos + while s.pos < s.len and _is_bare_key_char(s.src[s.pos]): s.pos += 1 + return _copy_span(s.pool, s.src, start, s.pos - start) + +# Dotted key path into segs; returns the segment count, -1 on error. +def _parse_key_path(s: TomlState*, segs: [TOML_MAX_KEY_SEGMENTS]str, cap: int) -> int: + n: int = 0 + while True: + if n >= cap: return -1 + seg: str = _parse_key_segment(s) + if seg == None: return -1 + segs[n] = seg + n += 1 + _skip_spaces(s) + if _peek(s) != '.': return n + _advance(s) + +# Join segs[0..n) into a dotted path, prefixed by `prefix` when it is a non-empty path. +def _join_path(pool: memhub.MemManager*, prefix: str, segs: [TOML_MAX_KEY_SEGMENTS]str, n: int) -> str: + total: sizet = 1 + if prefix != None: total += string.strlen(prefix) + 1 + i: int = 0 + while i < n: + total += string.strlen(segs[i]) + 1 + i += 1 + buf: str = pool.alloc(total) + if buf == None: return None + di: sizet = 0 + if prefix != None and prefix[0] != '\0': + plen: sizet = string.strlen(prefix) + string.memcpy(buf, prefix, plen) + di = plen + buf[di] = '.' + di += 1 + i = 0 + while i < n: + if i > 0: buf[di] = '.'; di += 1 + slen: sizet = string.strlen(segs[i]) + string.memcpy(buf + di, segs[i], slen) + di += slen + i += 1 + buf[di] = '\0' + return buf + +# ============================================================ +# Explicitly-defined table registry (TOML forbids defining a table twice) +# ============================================================ + +def _defs_init(s: TomlState*) -> int: + s.defs = s.pool.alloc(TOML_DEFS_CAP) + if s.defs == None: return -1 + s.defs[0] = '\0' + s.defs_len = 0 + return 0 + +def _defs_contains(s: TomlState*, path: str) -> int: + if s.defs == None or path == None: return 0 + plen: sizet = string.strlen(path) + pat: str = s.pool.alloc(plen + 3) + if pat == None: return 0 + pat[0] = '\n' + string.memcpy(pat + 1, path, plen) + pat[plen + 1] = '\n' + pat[plen + 2] = '\0' + if string.strstr(s.defs, pat) != None: return 1 + return 0 + +def _defs_add(s: TomlState*, path: str) -> int: + if s.defs == None or path == None: return -1 + plen: sizet = string.strlen(path) + if s.defs_len + plen + 3 > TOML_DEFS_CAP: return -1 + s.defs[s.defs_len] = '\n' + s.defs_len += 1 + string.memcpy(s.defs + s.defs_len, path, plen) + s.defs_len += plen + s.defs[s.defs_len] = '\n' + s.defs_len += 1 + s.defs[s.defs_len] = '\0' + return 0 + +def _mark_defined(s: TomlState*, path: str) -> int: + if _defs_contains(s, path) != 0: return -1 + return _defs_add(s, path) + + +# ============================================================ +# Table navigation +# ============================================================ + +# Child table under `parent`: existing object, or a freshly created one. None on a name clash. +def _child_table(pool: memhub.MemManager*, parent: JsonValue*, key: str) -> JsonValue*: + if parent == None or key == None: return None + exist: JsonValue* = parent[key] + if exist != None: + if exist.vtype == JSON_OBJECT: return exist + return None + fresh: JsonValue* = object(pool) + if fresh == None: return None + object_set(pool, parent, key, fresh) + if parent[key] == None: return None # key buffer allocation failed + return fresh + +# Walk segs[0..n-1) creating intermediate tables. With mark != 0 a **newly created** intermediate +# is registered as explicitly defined (a dotted key defines its table, so a later [table] header for +# the same path is an error), while a reused table is left alone: `a.b = 1` then `a.c = 2` is legal. +# Inline-table keys pass mark = 0 because they are outside the table registry. +def _dotted_target(s: TomlState*, base: JsonValue*, segs: [TOML_MAX_KEY_SEGMENTS]str, n: int, mark: int) -> JsonValue*: + tbl: JsonValue* = base + i: int = 0 + while i < n - 1: + existed: int = 0 + raw: JsonValue* = tbl[segs[i]] + if raw != None and raw.vtype == JSON_OBJECT: existed = 1 + nxt: JsonValue* = _child_table(s.pool, tbl, segs[i]) + if nxt == None: return None + if mark != 0 and existed == 0 and s.in_array == 0: + sub: str = _join_path(s.pool, s.cur_path, segs, i + 1) + if sub == None: return None + if _mark_defined(s, sub) != 0: return None + tbl = nxt + i += 1 + return tbl + +# [[key]]: append a new object element to the array `key` of `parent`, creating the array first. +def _array_element_new(pool: memhub.MemManager*, parent: JsonValue*, key: str) -> JsonValue*: + if parent == None or key == None: return None + arr: JsonValue* = parent[key] + if arr == None: + arr = array(pool) + if arr == None: return None + object_set(pool, parent, key, arr) + arr = parent[key] + if arr == None: return None + elif arr.vtype != JSON_ARRAY: + return None + elem: JsonValue* = object(pool) + if elem == None: return None + arr._append_child(elem) + return elem + + +# ============================================================ +# Statements +# ============================================================ + +# [a.b.c] - absolute path from the root; the parent chain must already be tables. +# [[a.b.c]] - the last segment is an array of tables: append a new element and switch to it. +def _parse_table_header(s: TomlState*) -> int: + _advance(s) # '[' + is_arr: int = 0 + if _peek(s) == '[': is_arr = 1; _advance(s) + segs: [TOML_MAX_KEY_SEGMENTS]str + n: int = _parse_key_path(s, segs, TOML_MAX_KEY_SEGMENTS) + if n <= 0: return -1 + _skip_spaces(s) + if _peek(s) != ']': return -1 + _advance(s) + if is_arr != 0: + if _peek(s) != ']': return -1 + _advance(s) + if _skip_line_tail(s) != 0: return -1 + parent: JsonValue* = s.root + i: int = 0 + while i < n - 1: + parent = _child_table(s.pool, parent, segs[i]) + if parent == None: return -1 + i += 1 + last: str = segs[n - 1] + if is_arr != 0: + elem: JsonValue* = _array_element_new(s.pool, parent, last) + if elem == None: return -1 + tail: str = _join_path(s.pool, "", segs, n) + if tail == None: return -1 + s.cur = elem + s.cur_path = tail + s.in_array = 1 + return 0 + path: str = _join_path(s.pool, "", segs, n) + if path == None: return -1 + if _mark_defined(s, path) != 0: return -1 + tbl: JsonValue* = _child_table(s.pool, parent, last) + if tbl == None: return -1 + s.cur = tbl + s.cur_path = path + s.in_array = 0 + return 0 + +# key = value, relative to the current table; dotted keys create intermediate tables. +def _parse_keyval(s: TomlState*) -> int: + segs: [TOML_MAX_KEY_SEGMENTS]str + n: int = _parse_key_path(s, segs, TOML_MAX_KEY_SEGMENTS) + if n <= 0: return -1 + _skip_spaces(s) + if _peek(s) != '=': return -1 + _advance(s) + val: JsonValue* = _parse_value(s) + if val == None: return -1 + tbl: JsonValue* = _dotted_target(s, s.cur, segs, n, 1) + if tbl == None: return -1 + last: str = segs[n - 1] + if tbl[last] != None: return -1 # duplicate key + object_set(s.pool, tbl, last, val) + if tbl[last] == None: return -1 + return _skip_line_tail(s) + + +# ============================================================ +# Entry point +# ============================================================ + +def parse(pool: memhub.MemManager*, src: str) -> JsonValue*: + """Parse a TOML document into a JsonValue tree (JSON_NULL node on failure, None only if the pool is exhausted).""" + if src == None: return null(pool) + s: TomlState* = TomlState(src, pool) + if _defs_init(s) != 0: return null(pool) + s.root = object(pool) + if s.root == None: return null(pool) + s.cur = s.root + s.cur_path = "" + failed: int = 0 + while True: + _skip_filler(s) + if s.pos >= s.len: break + if _peek(s) == '[': + if _parse_table_header(s) != 0: failed = 1; break + else: + if _parse_keyval(s) != 0: failed = 1; break + if failed != 0: return null(pool) + return s.root