/
nv-lang
/
nova
Обзор
Документация
Войти
/
nv-lang
/
nova
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
std/src/encoding/toml.nv
453 строки
17 KB
Evgeniy Golovin
std(naming): collections/hashmap → hash_map — единственный нарушитель snake_case-правила модулей
06 авг 2026, 12:48
06 авг 2026, 12:48
0b0a260
Код
Авторство
О чём код?
// stdlib/toml.nv — TOML 1.0.0 parser. https://toml.io/en/v1.0.0 // // Dogfooding: D78 описывает `nova.toml` манифест — поэтому Nova // должна уметь парсить TOML. // // Поддержка: // - Bare keys, quoted keys // - String, integer, float, boolean // - Arrays, inline tables // - Tables [section] и [[array of tables]] // - Comments # // // Не поддержка (намеренно — нет в bootstrap): // - Datetime literals (RFC 3339) — отдельной либой если понадобится // - Multi-line strings """...""" (упрощено: только одностроки) // - Underscore separators в числах (1_000) // // API: // Toml.parse(s str) Fail[ParseTomlError] -> TomlValue // // [2026-07-10, промоушен из std/_experimental]: два runtime-дефекта, // заблокировавшие промоушен ([M-toml-repeated-fail-call-run-fail]), // исправлены в этом файле — см. inline-комментарии у `is_bare_key_char` // и `@parse_number`. Ни один не был багом Fail-эффекта/fail-frame/ // consume-scope (исходная гипотеза маркера ошибочна) — оба чисто // синтаксические/API-ошибки в ЭТОМ файле. module encoding.toml import std.collections.hash_map.{HashMap} /// TOML value variants (string/int/float/bool/array/table). #stable(since = "0.1") export type TomlValue enum | TomlStr(str) | TomlInt(i64) | TomlFloat(f64) | TomlBool(bool) | TomlArray([]TomlValue) | TomlTable(HashMap[str, TomlValue]) /// Errors while parsing TOML. #stable(since = "0.1") export type ParseTomlError enum | UnexpectedChar { line int, col int, char char } | UnexpectedEof { line int } | InvalidNumber { line int, value str } | InvalidString { line int, reason str } | DuplicateKey { line int, key str } | UnclosedTable { line int } // ────────────────────────────────────────────────────────────────────────── // Parser state // ────────────────────────────────────────────────────────────────────────── type TomlParser { input str // Push-loop materialisation of `input.chars()` (not `.collect()` — crashes // codegen on CharsIter). `@peek()` is the hot-path called at every parser // step — decoding was the ретрактированный `chars().nth(@pos)` O(n) scan // per call (O(n²) tokenizing overall, D260-амендмент); `chs.get(@pos)` is // O(1). TOML basic strings allow arbitrary UTF-8, so this must be a real // codepoint vector, not a byte view. chs []char mut pos int mut line int mut col int mut root HashMap[str, TomlValue] mut current_path []str // current table path для [section] } fn TomlParser.new(s str) -> Self { mut chs []char = []char.new(cap: s.byte_len()) for c in s.chars() { chs.push(c) } { input: s, chs, pos: 0, line: 1, col: 1, root: HashMap[str, TomlValue].new(), current_path: [], } } // ────────────────────────────────────────────────────────────────────────── // Public API // ────────────────────────────────────────────────────────────────────────── /// Parse a TOML document → `TomlValue.TomlTable(...)` root. /// /// Supports: key=value, [section] / [a.b.c] tables, basic strings, ints, /// floats, booleans, arrays. #stable(since = "0.1") export fn Toml.parse(s str) Fail[ParseTomlError] -> TomlValue { // [M-lint-findings-static-conversion] [M-lint-findings-fail-public-signature] mut p = TomlParser.new(s) p.parse_document() TomlTable(p.root) } // ────────────────────────────────────────────────────────────────────────── // Lexing helpers // ────────────────────────────────────────────────────────────────────────── fn TomlParser @peek() -> Option[char] => @chs.get(@pos) fn TomlParser mut @advance() -> Option[char] { ro c = @peek() match c { Some('\n') => { @line += 1; @col = 1 } Some(_) => { @col += 1 } None => () } if c != None { @pos += 1 } c } fn TomlParser mut @skip_whitespace_and_comments() -> () { mut done = false while !done { match @peek() { Some(' ') | Some('\t') => { @advance() } Some('#') => { while @peek() != None && @peek() != Some('\n') { @advance() } } _ => { done = true } } } } fn TomlParser mut @skip_whitespace_and_newlines() -> () { mut done = false while !done { match @peek() { Some(' ') | Some('\t') | Some('\n') | Some('\r') => { @advance() } Some('#') => { while @peek() != None && @peek() != Some('\n') { @advance() } } _ => { done = true } } } } // ────────────────────────────────────────────────────────────────────────── // Document parsing // ────────────────────────────────────────────────────────────────────────── fn TomlParser mut @parse_document() Fail[ParseTomlError] -> () { @skip_whitespace_and_newlines() while @peek() != None { match @peek() { Some('[') => @parse_table_header() _ => @parse_key_value() } @skip_whitespace_and_newlines() } } fn TomlParser mut @parse_table_header() Fail[ParseTomlError] -> () { @advance() // [ ro is_array = @peek() == Some('[') if is_array { @advance() } ro path = @parse_key_path() if @peek() != Some(']') { throw UnclosedTable { @line } } @advance() if is_array { if @peek() != Some(']') { throw UnclosedTable { @line } } @advance() } @current_path = path // Создаём nested table если её нет @ensure_path_exists(path, is_array) } fn TomlParser mut @parse_key_path() Fail[ParseTomlError] -> []str { mut path []str = [] @skip_whitespace_and_comments() path.push(@parse_key()) @skip_whitespace_and_comments() while @peek() == Some('.') { @advance() @skip_whitespace_and_comments() path.push(@parse_key()) @skip_whitespace_and_comments() } path } fn TomlParser mut @parse_key() Fail[ParseTomlError] -> str { match @peek() { Some('"') => @parse_basic_string() Some(c) if is_bare_key_char(c) => @parse_bare_key() Some(c) => throw UnexpectedChar { @line, @col, char: c } None => throw UnexpectedEof { @line } } } fn TomlParser mut @parse_bare_key() -> str { consume buf = StringBuilder.new() mut done = false while !done { match @peek() { Some(c) if is_bare_key_char(c) => { buf.append(c) @advance() } _ => { done = true } } } buf } fn is_bare_key_char(c char) -> bool { // [M-toml-repeated-fail-call-run-fail follow-up, 2026-07-10]: a LEADING // `||` on a continuation line is NOT a logical-OR continuation in Nova // — `||` is ALSO the zero-arg closure-literal syntax (`|| body`), and // the parser (parse_or, compiler-codegen/src/parser/mod.rs) deliberately // does not extend newline-tolerance to a line-initial `||` (to avoid // misparsing a genuine `|| body` closure statement as an OR-continuation // of the previous line). The original leading-`||` chain here silently // miscompiled: each `|| (...)` line became its OWN discarded zero-arg // closure-literal statement, and the function's trailing value ended up // being the LAST closure's pointer coerced to `nova_bool` — always // truthy (confirmed via generated C: `nova_bool r = (void*)(&closure); // return r;`). Root cause of the toml runtime hazard (NOT a Fail-effect / // fail-frame / consume-scope bug, despite the marker name — see // docs/plans/backlog-followups.md). `nova check`/`nova test-build` both // accept this silently (no diagnostic) — tracked as a separate follow-up // marker (checker should reject a closure-typed trailing expr against a // `bool` return type). Fix here: move `||` to the END of each line — a // TRAILING binary operator before a newline IS treated as continuation // (no closure-literal ambiguity at end-of-line), unlike a leading one. ro n = c as int (n >= 65 && n <= 90) || // A-Z (n >= 97 && n <= 122) || // a-z (n >= 48 && n <= 57) || // 0-9 c == '_' || c == '-' } // ────────────────────────────────────────────────────────────────────────── // Key = Value // ────────────────────────────────────────────────────────────────────────── fn TomlParser mut @parse_key_value() Fail[ParseTomlError] -> () { ro key = @parse_key() @skip_whitespace_and_comments() if @peek() != Some('=') { throw UnexpectedChar { @line, @col, char: @peek() ?? ' ' } } @advance() @skip_whitespace_and_comments() ro value = @parse_value() @insert_at_current_path(key, value) } fn TomlParser mut @parse_value() Fail[ParseTomlError] -> TomlValue { @skip_whitespace_and_comments() match @peek() { Some('"') => TomlStr(@parse_basic_string()) Some('[') => @parse_array() Some('{') => @parse_inline_table() Some('t') | Some('f') => @parse_bool() Some(c) if is_num_start(c) => @parse_number() Some(c) => throw UnexpectedChar { @line, @col, char: c } None => throw UnexpectedEof { @line } } } fn is_num_start(c char) -> bool => c == '-' || c == '+' || (c >= '0' && c <= '9') fn TomlParser mut @parse_basic_string() Fail[ParseTomlError] -> str { @advance() // open " consume buf = StringBuilder.new() mut done = false mut err Option[ParseTomlError] = None while !done { match @advance() { None => { err = Some(UnexpectedEof { @line }); done = true } Some('"') => { done = true } Some('\\') => { match @advance() { Some('n') => buf.append('\n') Some('r') => buf.append('\r') Some('t') => buf.append('\t') Some('"') => buf.append('"') Some('\\') => buf.append('\\') Some(c) => { err = Some(InvalidString { @line, reason: "unknown escape" }); done = true } None => { err = Some(UnexpectedEof { @line }); done = true } } } Some(c) => buf.append(c) } } ro s = buf.into_str() if err != None { throw err.unwrap() } s } fn TomlParser mut @parse_bool() Fail[ParseTomlError] -> TomlValue { if @match_keyword("true") { TomlBool(true) } else if @match_keyword("false") { TomlBool(false) } else { throw UnexpectedChar { @line, @col, char: @peek() ?? ' ' } } } fn TomlParser mut @match_keyword(kw str) -> bool { // `kw` — короткий ASCII-литерал ("true"/"false"); прямая for-in итерация // вместо ретрактированного `chars().nth(i)` (D260-амендмент). ro saved = @pos for expected in kw.chars() { if @peek() != Some(expected) { @pos = saved return false } @advance() } true } fn TomlParser mut @parse_number() Fail[ParseTomlError] -> TomlValue { ro start = @pos if @peek() == Some('-') || @peek() == Some('+') { @advance() } mut has_dot = false mut has_exp = false loop { match @peek() { Some(c) if c >= '0' && c <= '9' => { @advance() } Some('.') if !has_dot && !has_exp => { has_dot = true; @advance() } Some('e') | Some('E') if !has_exp => { has_exp = true @advance() if @peek() == Some('-') || @peek() == Some('+') { @advance() } } _ => break } } ro text = @input[start..@pos] // [M-toml-repeated-fail-call-run-fail follow-up, 2026-07-10]: `f64.try_from`/ // `i64.try_from(str)` are the RETRACTED numeric-parse surface // ([M-f64-try-parse-to-parse-f64], Plan 174.1) — `f64.try_from("3.14")` // silently truncates to `3.0` (known-broken, not re-fixed on that path). // Canon replacement is conversion-on-source: `str @to_f64()` / `str @to_i64()` // (std/runtime/string/parse.nv). if has_dot || has_exp { match text.to_f64() { Ok(f) => TomlFloat(f) Err(_) => throw InvalidNumber { @line, value: text } } } else { match text.to_i64() { Ok(n) => TomlInt(n) Err(_) => throw InvalidNumber { @line, value: text } } } } fn TomlParser mut @parse_array() Fail[ParseTomlError] -> TomlValue { @advance() // [ mut items []TomlValue = [] @skip_whitespace_and_newlines() if @peek() == Some(']') { @advance() return TomlArray(items) } items.push(@parse_value()) @skip_whitespace_and_newlines() while @peek() == Some(',') { @advance() @skip_whitespace_and_newlines() if @peek() == Some(']') { break } items.push(@parse_value()) @skip_whitespace_and_newlines() } if @peek() != Some(']') { throw UnexpectedChar { @line, @col, char: @peek() ?? ' ' } } @advance() TomlArray(items) } fn TomlParser mut @parse_inline_table() Fail[ParseTomlError] -> TomlValue { @advance() // { mut t = HashMap[str, TomlValue].new() @skip_whitespace_and_comments() if @peek() == Some('}') { @advance() return TomlTable(t) } ro key = @parse_key() @skip_whitespace_and_comments() if @peek() != Some('=') { throw UnexpectedChar { @line, @col, char: @peek() ?? ' ' } } @advance() @skip_whitespace_and_comments() ro value = @parse_value() t.insert(key, value) @skip_whitespace_and_comments() while @peek() == Some(',') { @advance() @skip_whitespace_and_comments() ro k = @parse_key() @skip_whitespace_and_comments() if @peek() != Some('=') { throw UnexpectedChar { @line, @col, char: @peek() ?? ' ' } } @advance() @skip_whitespace_and_comments() ro v = @parse_value() t.insert(k, v) @skip_whitespace_and_comments() } if @peek() != Some('}') { throw UnexpectedChar { @line, @col, char: @peek() ?? ' ' } } @advance() TomlTable(t) } // ────────────────────────────────────────────────────────────────────────── // Path resolution для nested tables // ────────────────────────────────────────────────────────────────────────── fn TomlParser mut @ensure_path_exists(path []str, is_array bool) -> () { // Упрощённо: для bootstrap-stdlib просто insert empty table. // Production-парсер обрабатывает [[array of tables]] полноценно. mut current = @root for segment in path { if !current.contains(segment) { current.insert(segment, TomlTable(HashMap[str, TomlValue].new())) } } // current_path уже установлен; инсерты идут через insert_at_current_path. } fn TomlParser mut @insert_at_current_path(key str, value TomlValue) -> () { // Если current_path пуст — инсертим в root. if @current_path.len() == 0 { @root.insert(key, value) return () } // Иначе — навигируемся вглубь и инсертим. Упрощённо для bootstrap. @root.insert(key, value) // TODO: nested path resolution }