chore: 纳入 ctraits.vpnci 与 tomllib/(与已删除的 ctraits.vpi 配套)
This commit is contained in:
1 parent
e25e8958f0
commit
77c8d4f9e4
3 files changed
+743
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user