///|
/// A parsed TOML value.
///
/// `TDatetime` carries the exact source spelling of an offset date-time, a
/// local date-time, a local date or a local time. No date arithmetic and no
/// timezone normalization is performed, so `1979-05-27 07:32:00Z` keeps both
/// its separator and its `Z`.
pub(all) enum TomlValue {
TString(String)
TInt(Int64)
TFloat(Double)
TBool(Bool)
TDatetime(String)
TArray(Array[TomlValue])
TTable(Toml)
} derive(Eq, @debug.Debug)
///|
pub extend TomlValue with Eq::{equal, not_equal}
///|
pub extend TomlValue with @debug.Debug::{to_repr}
///|
/// An ordered TOML table.
///
/// Entries are kept as an association list in document order. Nothing is ever
/// re-sorted, so `Toml::to_string` is deterministic; lookups are a linear
/// scan, which is fine for the small tables found in `pyproject.toml` and
/// `pylock.toml`.
pub(all) struct Toml {
entries : Array[(String, TomlValue)]
} derive(Eq, @debug.Debug)
///|
pub extend Toml with Eq::{equal, not_equal}
///|
pub extend Toml with @debug.Debug::{to_repr}
///|
/// Mutable table used while parsing. `Array` is a shared reference, so the
/// same `MTable` value can be extended from several places.
priv struct MTable {
entries : Array[(String, MEntry)]
}
///|
/// One slot of a mutable table. `explicit` holds the TOML 1.0 "explicitly
/// defined" flag: the name came from a `[table]` header or from a dotted key,
/// so it may not be declared a second time.
priv struct MEntry {
node : MNode
mut explicit : Bool
}
///|
/// A slot either holds a finished value, a structural sub-table, or the list
/// of tables built by `[[name]]` headers.
priv enum MNode {
MValue(TomlValue)
MSub(MTable)
MTables(Array[MTable])
}
///|
fn new_table() -> MTable {
{ entries: [], }
}
///|
fn new_entry(node : MNode) -> MEntry {
{ node, explicit: false, }
}
///|
fn MTable::find(self : MTable, key : String) -> MEntry? {
for i = 0; i < self.entries.length(); i = i + 1 {
if self.entries[i].0 == key {
return Some(self.entries[i].1)
}
}
None
}
///|
/// Copy of a path with the first `count` components of `key` appended.
fn concat_path(
prefix : Array[String],
key : Array[String],
count : Int,
) -> Array[String] {
let out : Array[String] = []
for i = 0; i < prefix.length(); i = i + 1 {
out.push(prefix[i])
}
for i = 0; i < count; i = i + 1 {
out.push(key[i])
}
out
}
///|
fn push_path(path : Array[String], key : String) -> Array[String] {
let out : Array[String] = []
for i = 0; i < path.length(); i = i + 1 {
out.push(path[i])
}
out.push(key)
out
}
///|
fn is_ws(c : Int) -> Bool {
c == 32 || c == 9
}
///|
fn is_digit_int(c : Int) -> Bool {
c >= 48 && c <= 57
}
///|
fn is_hex_int(c : Int) -> Bool {
(c >= 48 && c <= 57) || (c >= 65 && c <= 70) || (c >= 97 && c <= 102)
}
///|
fn is_base_digit(c : Int, base : Int) -> Bool {
if base == 16 {
is_hex_int(c)
} else if base == 8 {
c >= 48 && c <= 55
} else {
c == 48 || c == 49
}
}
///|
fn is_bare_key_int(c : Int) -> Bool {
(c >= 48 && c <= 57) ||
(c >= 65 && c <= 90) ||
(c >= 97 && c <= 122) ||
c == 95 ||
c == 45
}
///|
/// TOML 1.0 forbids raw control characters in strings; tabs are always fine
/// and newlines are fine inside multi-line strings. A lone `\r` never reaches
/// this check because `\r\n` is normalized away before parsing starts.
fn is_illegal_str_char(c : Int, multiline : Bool) -> Bool {
if c == 9 {
false
} else if multiline && c == 10 {
false
} else {
c < 32 || c == 127
}
}
///|
/// Recursive-descent TOML 1.0 reader over one CRLF-normalized document. The
/// parser walks the source code unit by code unit and closes containers as it
/// meets their terminator, which keeps multi-line strings and multi-line
/// arrays uniform.
priv struct Parser {
src : String
mut pos : Int
root : MTable
mut current_path : Array[String]
mut pending : Array[Array[String]]
}
///|
fn new_parser(src : String) -> Parser {
let root = new_table()
{ src, pos: 0, root, current_path: [], pending: [], }
}
///|
/// The `Int` carried by `VersionError::InvalidToml` is a UTF-16 offset into
/// the CRLF-normalized source, matching the offsets used elsewhere in this
/// module.
fn toml_error(code : String, pos : Int) -> VersionError {
VersionError::InvalidToml(code, pos)
}
///|
fn[T] Parser::fail(self : Parser, code : String) -> T raise VersionError {
raise toml_error(code, self.pos)
}
///|
/// Code unit at the cursor, or `-1` at end of input.
fn Parser::peek(self : Parser) -> Int {
if self.pos >= self.src.length() {
-1
} else {
self.src[self.pos].to_int()
}
}
///|
fn Parser::peek_at(self : Parser, offset : Int) -> Int {
let i = self.pos + offset
if i < 0 || i >= self.src.length() {
-1
} else {
self.src[i].to_int()
}
}
///|
fn Parser::eof(self : Parser) -> Bool {
self.pos >= self.src.length()
}
///|
fn Parser::advance(self : Parser) -> Unit {
self.pos = self.pos + 1
}
///|
fn Parser::starts_with(self : Parser, text : String) -> Bool {
let n = text.length()
if self.pos + n > self.src.length() {
false
} else {
self.src[self.pos:self.pos + n].to_owned() == text
}
}
///|
fn Parser::skip_ws(self : Parser) -> Unit {
while is_ws(self.peek()) {
self.advance()
}
}
///|
/// Skip spaces, tabs, newlines and `#` comments. Inside arrays all four are
/// allowed between elements.
fn Parser::skip_array_ws(self : Parser) -> Unit {
while true {
let before = self.pos
while is_ws(self.peek()) || self.peek() == 10 {
self.advance()
}
self.skip_comment()
if self.pos == before {
return
}
}
}
///|
fn Parser::skip_comment(self : Parser) -> Unit {
if self.peek() == 35 {
while !self.eof() && self.peek() != 10 {
self.advance()
}
}
}
///|
/// Slot at `path`, relative to `start`; `None` when it does not exist.
/// Walking *through* a finished value is an error, which is how TOML forbids
/// extending a namespace that already holds a value (an inline table or an
/// array, for instance).
fn Parser::entry_at_from(
self : Parser,
start : MTable,
path : Array[String],
) -> MEntry? raise VersionError {
if path.length() == 0 {
return None
}
let mut table = start
for i = 0; i < path.length() - 1; i = i + 1 {
match table.find(path[i]) {
Some(entry) =>
match entry.node {
MSub(sub) => table = sub
MTables(tables) => table = tables[tables.length() - 1]
MValue(_) => self.fail("CANNOT_OVERWRITE_VALUE")
}
None => return None
}
}
table.find(path[path.length() - 1])
}
///|
/// Table at `path`, creating empty sub-tables as needed.
fn Parser::ensure_table_from(
self : Parser,
start : MTable,
path : Array[String],
) -> MTable raise VersionError {
let mut table = start
for i = 0; i < path.length(); i = i + 1 {
let key = path[i]
match table.find(key) {
Some(entry) =>
match entry.node {
MSub(sub) => table = sub
MTables(tables) => table = tables[tables.length() - 1]
MValue(_) => self.fail("CANNOT_OVERWRITE_VALUE")
}
None => {
let sub = new_table()
table.entries.push((key, new_entry(MSub(sub))))
table = sub
}
}
}
table
}
///|
/// Turn pending dotted-key namespaces into explicit ones. Dotted keys only
/// lock a name once a later `[header]` is seen; that is what makes
/// `a.b = 1` followed by `a.c = 2` legal while a following `[a.b]` is not.
fn Parser::finalize_pending(self : Parser) -> Unit raise VersionError {
for i = 0; i < self.pending.length(); i = i + 1 {
let path = self.pending[i]
match self.entry_at_from(self.root, path) {
Some(entry) => entry.explicit = true
None => ()
}
}
self.pending = []
}
///|
fn Parser::clear_explicit_table(self : Parser, table : MTable) -> Unit {
for i = 0; i < table.entries.length(); i = i + 1 {
self.clear_explicit_entry(table.entries[i].1)
}
}
///|
/// `[[a]]` re-opens `a`, so every declaration flag below it is dropped. This
/// mirrors the reference implementation, where a second `[[a]]` resets the
/// flags of everything nested under `a`.
fn Parser::clear_explicit_entry(self : Parser, entry : MEntry) -> Unit {
entry.explicit = false
match entry.node {
MValue(_) => ()
MSub(sub) => self.clear_explicit_table(sub)
MTables(tables) =>
for i = 0; i < tables.length(); i = i + 1 {
self.clear_explicit_table(tables[i])
}
}
}
///|
fn Parser::is_explicit(
self : Parser,
path : Array[String],
) -> Bool raise VersionError {
match self.entry_at_from(self.root, path) {
Some(entry) => entry.explicit
None => false
}
}
///|
/// `key = value`, relative to the enclosing table header.
fn Parser::parse_key_value(self : Parser) -> Unit raise VersionError {
let key = self.parse_key()
self.skip_ws()
if self.peek() != 61 {
self.fail("EXPECTED_EQUALS_AFTER_KEY")
}
self.advance()
self.skip_ws()
let value = self.parse_value(0)
let count = key.length()
for i = 1; i < count; i = i + 1 {
let container = concat_path(self.current_path, key, i)
if self.is_explicit(container) {
self.fail("CANNOT_REDEFINE_NAMESPACE")
}
self.pending.push(container)
}
let parent = self.ensure_table_from(
self.root,
concat_path(self.current_path, key, count - 1),
)
let stem = key[count - 1]
match parent.find(stem) {
Some(_) => self.fail("DUPLICATE_KEY")
None => ()
}
parent.entries.push((stem, new_entry(MValue(value))))
}
///|
/// `[a.b]`.
fn Parser::parse_table_header(self : Parser) -> Unit raise VersionError {
self.advance()
self.skip_ws()
let key = self.parse_key()
self.skip_ws()
if self.peek() != 93 {
self.fail("EXPECTED_CLOSE_BRACKET")
}
self.advance()
if self.is_explicit(key) {
self.fail("TABLE_ALREADY_DEFINED")
}
let _ = self.ensure_table_from(self.root, key)
match self.entry_at_from(self.root, key) {
Some(entry) => entry.explicit = true
None => ()
}
self.current_path = key
}
///|
/// `[[a.b]]`, which appends a fresh table to the array behind the name.
fn Parser::parse_array_table_header(self : Parser) -> Unit raise VersionError {
self.advance()
self.advance()
self.skip_ws()
let key = self.parse_key()
self.skip_ws()
if self.peek() != 93 || self.peek_at(1) != 93 {
self.fail("EXPECTED_DOUBLE_CLOSE_BRACKET")
}
self.advance()
self.advance()
let count = key.length()
let parent = self.ensure_table_from(
self.root,
concat_path([], key, count - 1),
)
let stem = key[count - 1]
match parent.find(stem) {
Some(entry) =>
match entry.node {
MTables(tables) => {
self.clear_explicit_entry(entry)
entry.explicit = true
let fresh = new_table()
tables.push(fresh)
self.current_path = key
}
_ => self.fail("CANNOT_OVERWRITE_VALUE")
}
None => {
let fresh = new_table()
let tables : Array[MTable] = [fresh]
let entry = new_entry(MTables(tables))
entry.explicit = true
parent.entries.push((stem, entry))
self.current_path = key
}
}
}
///|
/// Whole document: table headers, key/value pairs, comments and blank lines.
fn Parser::parse_document(self : Parser) -> MTable raise VersionError {
while true {
self.skip_ws()
if self.eof() {
break
}
let c = self.peek()
if c == 10 {
self.advance()
continue
}
if c == 35 {
self.skip_comment()
} else if c == 91 {
self.finalize_pending()
if self.peek_at(1) == 91 {
self.parse_array_table_header()
} else {
self.parse_table_header()
}
self.skip_ws()
} else if is_bare_key_int(c) || c == 34 || c == 39 {
self.parse_key_value()
self.skip_ws()
} else {
self.fail("INVALID_STATEMENT")
}
self.skip_comment()
if self.eof() {
break
}
if self.peek() != 10 {
self.fail("EXPECTED_NEWLINE_OR_EOF")
}
self.advance()
}
self.root
}
///|
fn Parser::parse_key(self : Parser) -> Array[String] raise VersionError {
let out : Array[String] = []
while true {
out.push(self.parse_key_part())
self.skip_ws()
if self.peek() == 46 {
self.advance()
self.skip_ws()
continue
}
break
}
out
}
///|
fn Parser::parse_key_part(self : Parser) -> String raise VersionError {
let c = self.peek()
if c == 34 {
self.parse_basic_string(false, false)
} else if c == 39 {
self.parse_literal_string()
} else if is_bare_key_int(c) {
let start = self.pos
while is_bare_key_int(self.peek()) {
self.advance()
}
self.src[start:self.pos].to_owned()
} else {
self.fail("INVALID_KEY")
}
}
///|
/// Basic strings, single line and multi-line. Literal runs are copied straight
/// out of the source and escapes are appended as they are decoded.
fn Parser::parse_basic_string(
self : Parser,
multiline : Bool,
opener_consumed : Bool,
) -> String raise VersionError {
let out = StringBuilder()
if !opener_consumed {
self.advance()
}
let mut run = self.pos
while true {
if self.eof() {
self.fail("UNTERMINATED_STRING")
}
let c = self.peek()
if c == 34 {
if !multiline {
out.write_view(self.src[run:self.pos])
self.advance()
return out.to_string()
}
if self.starts_with("\"\"\"") {
out.write_view(self.src[run:self.pos])
self.pos = self.pos + 3
return out.to_string()
}
} else if c == 92 {
out.write_view(self.src[run:self.pos])
self.advance()
self.parse_escape(out, multiline)
run = self.pos
continue
} else if is_illegal_str_char(c, multiline) {
self.fail("ILLEGAL_CHARACTER_IN_STRING")
}
self.advance()
}
self.fail("UNTERMINATED_STRING")
}
///|
/// The cursor sits just after a backslash.
fn Parser::parse_escape(
self : Parser,
out : StringBuilder,
multiline : Bool,
) -> Unit raise VersionError {
if self.eof() {
self.fail("INVALID_ESCAPE")
}
let c = self.peek()
if multiline && (c == 32 || c == 9 || c == 10) {
// A backslash at the end of a line swallows the line break and every
// following whitespace-only line.
if c != 10 {
self.skip_ws()
if self.peek() != 10 {
self.fail("INVALID_ESCAPE")
}
self.advance()
} else {
self.advance()
}
while is_ws(self.peek()) || self.peek() == 10 {
self.advance()
}
return
}
if c == 34 {
self.advance()
out.write_string("\"")
} else if c == 92 {
self.advance()
out.write_string("\\")
} else if c == 98 {
self.advance()
out.write_string("\u{08}")
} else if c == 102 {
self.advance()
out.write_string("\u{0C}")
} else if c == 110 {
self.advance()
out.write_string("\n")
} else if c == 114 {
self.advance()
out.write_string("\r")
} else if c == 116 {
self.advance()
out.write_string("\t")
} else if c == 117 {
self.advance()
self.parse_hex_escape(out, 4)
} else if c == 85 {
self.advance()
self.parse_hex_escape(out, 8)
} else {
self.fail("INVALID_ESCAPE")
}
}
///|
fn Parser::parse_hex_escape(
self : Parser,
out : StringBuilder,
digits : Int,
) -> Unit raise VersionError {
let mut value = 0
for i = 0; i < digits; i = i + 1 {
let c = self.peek()
if !is_hex_int(c) {
self.fail("INVALID_ESCAPE")
}
value = value * 16 + toml_hex_value(c)
self.advance()
}
if value > 0x10FFFF || (value >= 0xD800 && value <= 0xDFFF) {
self.fail("INVALID_ESCAPE")
}
match value.to_char() {
Some(ch) => out.write_char(ch)
None => self.fail("INVALID_ESCAPE")
}
}
///|
fn toml_hex_value(c : Int) -> Int {
if c >= 48 && c <= 57 {
c - 48
} else if c >= 65 && c <= 70 {
c - 55
} else {
c - 87
}
}
///|
/// `"""..."""`: a newline immediately after the opener is dropped, and one or
/// two extra quotes may belong to the content.
fn Parser::parse_multiline_basic(self : Parser) -> String raise VersionError {
// Consume the three opening quotes and a single immediately following
// newline, which TOML trims. The cursor is then already inside the string, so
// `parse_basic_string` must not skip a character of its own.
self.pos = self.pos + 3
if self.peek() == 10 {
self.advance()
}
let mut text = self.parse_basic_string(true, true)
if self.peek() == 34 {
self.advance()
text = text + "\""
if self.peek() == 34 {
self.advance()
text = text + "\""
}
}
text
}
///|
/// `'''...'''`. No escape processing at all.
fn Parser::parse_multiline_literal(self : Parser) -> String raise VersionError {
self.pos = self.pos + 3
if self.peek() == 10 {
self.advance()
}
let start = self.pos
let mut text = ""
let mut closed = false
while !closed {
if self.eof() {
self.fail("UNTERMINATED_STRING")
}
let c = self.peek()
if c == 39 && self.starts_with("'''") {
text = self.src[start:self.pos].to_owned()
self.pos = self.pos + 3
closed = true
} else if is_illegal_str_char(c, true) {
self.fail("ILLEGAL_CHARACTER_IN_STRING")
} else {
self.advance()
}
}
if self.peek() == 39 {
self.advance()
text = text + "'"
if self.peek() == 39 {
self.advance()
text = text + "'"
}
}
text
}
///|
/// `'...'`, single line.
fn Parser::parse_literal_string(self : Parser) -> String raise VersionError {
self.advance()
let start = self.pos
while true {
if self.eof() {
self.fail("UNTERMINATED_STRING")
}
let c = self.peek()
if c == 39 {
let text = self.src[start:self.pos].to_owned()
self.advance()
return text
}
if is_illegal_str_char(c, false) {
self.fail("ILLEGAL_CHARACTER_IN_STRING")
}
self.advance()
}
self.fail("UNTERMINATED_STRING")
}
///|
/// Offset date-time, local date-time, local date or local time. Returns `None`
/// when the text is not shaped like a date or a time at all, so that the
/// caller can fall through to number parsing.
fn Parser::parse_datetime(self : Parser) -> TomlValue? raise VersionError {
if self.looks_like_date() {
let start = self.pos
let year = self.read_int(4)
self.advance()
let month = self.read_int(2)
self.advance()
let day = self.read_int(2)
if month < 1 || month > 12 || day < 1 || day > days_in_month(year, month) {
self.fail("INVALID_DATETIME")
}
if self.is_time_delimiter() && is_digit_int(self.peek_at(1)) {
self.advance()
let _ = self.read_time_text()
self.parse_offset()
}
Some(TDatetime(self.src[start:self.pos].to_owned()))
} else if self.looks_like_time() {
Some(TDatetime(self.read_time_text()))
} else {
None
}
}
///|
fn Parser::looks_like_date(self : Parser) -> Bool {
is_digit_int(self.peek()) &&
is_digit_int(self.peek_at(1)) &&
is_digit_int(self.peek_at(2)) &&
is_digit_int(self.peek_at(3)) &&
self.peek_at(4) == 45 &&
is_digit_int(self.peek_at(5)) &&
is_digit_int(self.peek_at(6)) &&
self.peek_at(7) == 45 &&
is_digit_int(self.peek_at(8)) &&
is_digit_int(self.peek_at(9))
}
///|
fn Parser::looks_like_time(self : Parser) -> Bool {
is_digit_int(self.peek()) &&
is_digit_int(self.peek_at(1)) &&
self.peek_at(2) == 58 &&
is_digit_int(self.peek_at(3)) &&
is_digit_int(self.peek_at(4)) &&
self.peek_at(5) == 58 &&
is_digit_int(self.peek_at(6)) &&
is_digit_int(self.peek_at(7))
}
///|
fn Parser::is_time_delimiter(self : Parser) -> Bool {
let c = self.peek()
c == 84 || c == 116 || c == 32
}
///|
fn Parser::read_int(self : Parser, digits : Int) -> Int {
let mut value = 0
for i = 0; i < digits; i = i + 1 {
value = value * 10 + (self.peek() - 48)
self.advance()
}
value
}
///|
/// `HH:MM:SS[.fraction]` starting at the cursor.
///
/// The fraction is capped at six digits, the precision a microsecond-based
/// implementation can represent; the specification leaves extra precision to
/// the implementation.
fn Parser::read_time_text(self : Parser) -> String raise VersionError {
if !self.looks_like_time() {
self.fail("INVALID_DATETIME")
}
let start = self.pos
let hour = self.read_int(2)
self.advance()
let minute = self.read_int(2)
self.advance()
let second = self.read_int(2)
if hour > 23 || minute > 59 || second > 59 {
self.fail("INVALID_DATETIME")
}
if self.peek() == 46 {
self.advance()
let mut seen = 0
while seen < 6 && is_digit_int(self.peek()) {
self.advance()
seen = seen + 1
}
if seen == 0 {
self.fail("INVALID_DATETIME")
}
}
self.src[start:self.pos].to_owned()
}
///|
/// Optional `Z`/`z` or `+HH:MM`/`-HH:MM` right after a time.
fn Parser::parse_offset(self : Parser) -> Unit raise VersionError {
let c = self.peek()
if c == 90 || c == 122 {
self.advance()
} else if c == 43 || c == 45 {
if !is_digit_int(self.peek_at(1)) ||
!is_digit_int(self.peek_at(2)) ||
self.peek_at(3) != 58 ||
!is_digit_int(self.peek_at(4)) ||
!is_digit_int(self.peek_at(5)) {
self.fail("INVALID_DATETIME")
}
self.advance()
let hour = self.read_int(2)
self.advance()
let minute = self.read_int(2)
if hour > 23 || minute > 59 {
self.fail("INVALID_DATETIME")
}
}
}
///|
fn days_in_month(year : Int, month : Int) -> Int {
if month == 2 {
if is_leap_year(year) {
29
} else {
28
}
} else if month == 4 || month == 6 || month == 9 || month == 11 {
30
} else {
31
}
}
///|
fn is_leap_year(year : Int) -> Bool {
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
///|
/// Integers and floats follow the TOML 1.0 grammar: no sign on `0x`/`0o`/`0b`,
/// no leading zeros on decimals, and `_` only between digits. Returns `None`
/// when the text does not start a number.
fn Parser::parse_number(self : Parser) -> TomlValue? raise VersionError {
let start = self.pos
let first = self.peek()
if first == 48 {
let marker = self.peek_at(1)
if marker == 120 || marker == 111 || marker == 98 {
let base = if marker == 120 { 16 } else if marker == 111 { 8 } else { 2 }
self.advance()
self.advance()
if !is_base_digit(self.peek(), base) {
// Nothing was consumed beyond the cursor, so the caller can still try
// another production at the original position.
self.pos = start
return None
}
self.advance()
while true {
if self.peek() == 95 && is_base_digit(self.peek_at(1), base) {
self.advance()
}
if is_base_digit(self.peek(), base) {
self.advance()
} else {
break
}
}
let digits = strip_underscores(self.src[start + 2:self.pos].to_owned())
return Some(self.make_int_in_base(digits, base))
}
}
if first == 43 || first == 45 {
self.advance()
}
let head = self.peek()
if head == 48 {
self.advance()
} else if head >= 49 && head <= 57 {
self.advance()
while true {
if self.peek() == 95 && is_digit_int(self.peek_at(1)) {
self.advance()
}
if is_digit_int(self.peek()) {
self.advance()
} else {
break
}
}
} else {
// Restore the sign, if one had been consumed: `-inf` is not a number, and
// the caller must still see the `-`.
self.pos = start
return None
}
let mut is_float = false
if self.peek() == 46 && is_digit_int(self.peek_at(1)) {
is_float = true
self.advance()
self.advance()
while true {
if self.peek() == 95 && is_digit_int(self.peek_at(1)) {
self.advance()
}
if is_digit_int(self.peek()) {
self.advance()
} else {
break
}
}
}
if self.peek() == 101 || self.peek() == 69 {
let mark = self.pos
self.advance()
if self.peek() == 43 || self.peek() == 45 {
self.advance()
}
if is_digit_int(self.peek()) {
is_float = true
self.advance()
while true {
if self.peek() == 95 && is_digit_int(self.peek_at(1)) {
self.advance()
}
if is_digit_int(self.peek()) {
self.advance()
} else {
break
}
}
} else {
self.pos = mark
}
}
let text = strip_underscores(self.src[start:self.pos].to_owned())
if is_float {
Some(self.make_float(text))
} else {
Some(self.make_int_decimal(text))
}
}
///|
fn Parser::make_int_in_base(
self : Parser,
digits : String,
base : Int,
) -> TomlValue raise VersionError {
// TOML integers are 64-bit signed values, so they are parsed as `Int64`; a
// 32-bit `Int` would reject `0xDEADBEEF` on a wasm32 target.
let value = @string.parse_int64(digits, base~) catch {
_ => self.fail("INTEGER_OUT_OF_RANGE")
}
TInt(value)
}
///|
fn Parser::make_int_decimal(
self : Parser,
text : String,
) -> TomlValue raise VersionError {
let digits = if text.has_prefix("+") {
text[1:text.length()].to_owned()
} else {
text
}
let value = @string.parse_int64(digits) catch {
_ => self.fail("INTEGER_OUT_OF_RANGE")
}
TInt(value)
}
///|
fn Parser::make_float(
self : Parser,
text : String,
) -> TomlValue raise VersionError {
let digits = if text.has_prefix("+") {
text[1:text.length()].to_owned()
} else {
text
}
let value = @string.parse_double(digits) catch {
_ => self.fail("INVALID_NUMBER")
}
TFloat(value)
}
///|
fn strip_underscores(text : String) -> String {
if !text.contains_char('_') {
return text
}
let out = StringBuilder()
for c in text {
if c != '_' {
out.write_char(c)
}
}
out.to_string()
}
///|
fn Parser::parse_value(
self : Parser,
depth : Int,
) -> TomlValue raise VersionError {
if depth > 256 {
self.fail("NESTING_TOO_DEEP")
}
self.skip_ws()
let c = self.peek()
if c == 34 {
if self.starts_with("\"\"\"") {
return TString(self.parse_multiline_basic())
}
return TString(self.parse_basic_string(false, false))
}
if c == 39 {
if self.starts_with("'''") {
return TString(self.parse_multiline_literal())
}
return TString(self.parse_literal_string())
}
if c == 116 && self.starts_with("true") {
self.pos = self.pos + 4
return TBool(true)
}
if c == 102 && self.starts_with("false") {
self.pos = self.pos + 5
return TBool(false)
}
if c == 91 {
return self.parse_array(depth)
}
if c == 123 {
return self.parse_inline_table(depth)
}
match self.parse_datetime() {
Some(value) => return value
None => ()
}
match self.parse_number() {
Some(value) => return value
None => ()
}
if self.starts_with("inf") {
self.pos = self.pos + 3
return TFloat(1.0 / 0.0)
}
if self.starts_with("nan") {
self.pos = self.pos + 3
return TFloat(0.0 / 0.0)
}
if self.starts_with("-inf") || self.starts_with("+inf") {
let negative = self.peek() == 45
self.pos = self.pos + 4
return TFloat(if negative { -1.0 / 0.0 } else { 1.0 / 0.0 })
}
if self.starts_with("-nan") || self.starts_with("+nan") {
self.pos = self.pos + 4
return TFloat(0.0 / 0.0)
}
self.fail("INVALID_VALUE")
}
///|
fn Parser::parse_array(
self : Parser,
depth : Int,
) -> TomlValue raise VersionError {
self.advance()
let items : Array[TomlValue] = []
self.skip_array_ws()
if self.peek() == 93 {
self.advance()
return TArray(items)
}
while true {
items.push(self.parse_value(depth + 1))
self.skip_array_ws()
let c = self.peek()
if c == 93 {
self.advance()
return TArray(items)
}
if c != 44 {
self.fail("UNCLOSED_ARRAY")
}
self.advance()
self.skip_array_ws()
if self.peek() == 93 {
self.advance()
return TArray(items)
}
}
self.fail("UNCLOSED_ARRAY")
}
///|
/// Inline tables are single-line in TOML 1.0: no newline between the braces
/// and no trailing comma. A value may of course span lines on its own, through
/// a multi-line string or an array.
fn Parser::parse_inline_table(
self : Parser,
depth : Int,
) -> TomlValue raise VersionError {
self.advance()
let table = new_table()
self.skip_ws()
self.reject_newline_in_inline_table()
if self.peek() == 125 {
self.advance()
return TTable(mtable_to_toml(table))
}
while true {
let key = self.parse_key()
self.skip_ws()
if self.peek() != 61 {
self.fail("EXPECTED_EQUALS_AFTER_KEY")
}
self.advance()
self.skip_ws()
let value = self.parse_value(depth + 1)
let count = key.length()
let parent = self.ensure_table_from(table, concat_path([], key, count - 1))
let stem = key[count - 1]
match parent.find(stem) {
Some(_) => self.fail("DUPLICATE_KEY")
None => ()
}
parent.entries.push((stem, new_entry(MValue(value))))
self.skip_ws()
let c = self.peek()
if c == 125 {
self.advance()
return TTable(mtable_to_toml(table))
}
if c != 44 {
self.fail("UNCLOSED_INLINE_TABLE")
}
self.advance()
self.skip_ws()
self.reject_newline_in_inline_table()
if self.peek() == 125 {
self.fail("TRAILING_COMMA_IN_INLINE_TABLE")
}
}
self.fail("UNCLOSED_INLINE_TABLE")
}
///|
/// TOML 1.0 keeps an inline table on one line, so a newline where another key
/// could start is a syntax error rather than an unterminated table. The two
/// cases are reported separately because the fixes differ: an unclosed table
/// needs a `}`, a wrapped table needs to become a `[table]`.
fn Parser::reject_newline_in_inline_table(
self : Parser,
) -> Unit raise VersionError {
if self.peek() == 10 {
self.fail("NEWLINE_IN_INLINE_TABLE")
}
}
///|
fn mtable_to_toml(table : MTable) -> Toml {
let entries : Array[(String, TomlValue)] = []
for i = 0; i < table.entries.length(); i = i + 1 {
entries.push((table.entries[i].0, mnode_to_value(table.entries[i].1.node)))
}
{ entries, }
}
///|
fn mnode_to_value(node : MNode) -> TomlValue {
match node {
MValue(value) => value
MSub(sub) => TTable(mtable_to_toml(sub))
MTables(tables) => {
let items : Array[TomlValue] = []
for i = 0; i < tables.length(); i = i + 1 {
items.push(TTable(mtable_to_toml(tables[i])))
}
TArray(items)
}
}
}
///|
/// Parse a TOML 1.0 document.
///
/// `\r\n` is normalized to `\n` first, which the specification allows even
/// inside string literals. A lone `\r` and a leading byte-order mark are
/// rejected.
pub fn Toml::parse(input : String) -> Toml raise VersionError {
let src = input.replace_all(old="\r\n", new="\n")
let parser = new_parser(src)
let root = parser.parse_document()
mtable_to_toml(root)
}
///|
fn split_dotted(key : String) -> Array[String] {
let out : Array[String] = []
let mut start = 0
for i = 0; i < key.length(); i = i + 1 {
if key[i].to_int() == 46 {
out.push(key[start:i].to_owned())
start = i + 1
}
}
out.push(key[start:key.length()].to_owned())
out
}
///|
fn Toml::find(self : Toml, key : String) -> TomlValue? {
for i = 0; i < self.entries.length(); i = i + 1 {
if self.entries[i].0 == key {
return Some(self.entries[i].1)
}
}
None
}
///|
/// Look up a key, optionally as a dotted path such as `"tool.poetry.name"`.
///
/// A dotted path descends through tables only; it stops at arrays. A key that
/// literally contains a `.` therefore has to be fetched through its parent
/// table.
pub fn Toml::get(self : Toml, key : String) -> TomlValue? {
let parts = split_dotted(key)
let mut table = self
for i = 0; i < parts.length() - 1; i = i + 1 {
match table.find(parts[i]) {
Some(TTable(sub)) => table = sub
_ => return None
}
}
table.find(parts[parts.length() - 1])
}
///|
pub fn Toml::get_string(self : Toml, key : String) -> String? {
match self.get(key) {
Some(TString(value)) => Some(value)
_ => None
}
}
///|
///|
/// An integer value, widened to `Int64` because that is TOML's own width. A
/// caller that knows the value fits can narrow it with `Int64::to_int`.
pub fn Toml::get_int(self : Toml, key : String) -> Int64? {
match self.get(key) {
Some(TInt(value)) => Some(value)
_ => None
}
}
///|
pub fn Toml::get_bool(self : Toml, key : String) -> Bool? {
match self.get(key) {
Some(TBool(value)) => Some(value)
_ => None
}
}
///|
pub fn Toml::get_array(self : Toml, key : String) -> Array[TomlValue]? {
match self.get(key) {
Some(TArray(value)) => Some(value)
_ => None
}
}
///|
pub fn Toml::get_table(self : Toml, key : String) -> Toml? {
match self.get(key) {
Some(TTable(value)) => Some(value)
_ => None
}
}
///|
fn is_bare_key_text(text : String) -> Bool {
if text.length() == 0 {
return false
}
for i = 0; i < text.length(); i = i + 1 {
if !is_bare_key_int(text[i].to_int()) {
return false
}
}
true
}
///|
fn render_key(text : String) -> String {
if is_bare_key_text(text) {
text
} else {
"\"" + escape_basic(text) + "\""
}
}
///|
fn escape_basic(text : String) -> String {
let out = StringBuilder()
for c in text {
let code = c.to_int()
if c == '"' {
out.write_string("\\\"")
} else if c == '\\' {
out.write_string("\\\\")
} else if code == 8 {
out.write_string("\\b")
} else if code == 9 {
out.write_string("\\t")
} else if code == 10 {
out.write_string("\\n")
} else if code == 12 {
out.write_string("\\f")
} else if code == 13 {
out.write_string("\\r")
} else if code < 32 || code == 127 {
out.write_string("\\u")
out.write_string(hex4(code))
} else {
out.write_char(c)
}
}
out.to_string()
}
///|
fn hex4(value : Int) -> String {
let digits = "0123456789ABCDEF"
let out = StringBuilder()
for shift = 12; shift >= 0; shift = shift - 4 {
let index = (value >> shift) & 15
out.write_view(digits[index:index + 1])
}
out.to_string()
}
///|
fn render_float(value : Double) -> String {
if value.is_nan() {
"nan"
} else if value.is_pos_inf() {
"inf"
} else if value.is_neg_inf() {
"-inf"
} else if value == 0.0 && 1.0 / value < 0.0 {
"-0.0"
} else {
let text = value.to_string()
if text.contains_char('.') ||
text.contains_char('e') ||
text.contains_char('E') {
text
} else {
text + ".0"
}
}
}
///|
fn render_value(value : TomlValue) -> String {
match value {
TString(text) => "\"" + escape_basic(text) + "\""
TInt(number) => number.to_string()
TFloat(number) => render_float(number)
TBool(flag) => if flag { "true" } else { "false" }
TDatetime(text) => text
TArray(items) => {
let out = StringBuilder()
out.write_string("[")
for i = 0; i < items.length(); i = i + 1 {
if i > 0 {
out.write_string(", ")
}
out.write_string(render_value(items[i]))
}
out.write_string("]")
out.to_string()
}
TTable(table) => {
let out = StringBuilder()
if table.entries.length() == 0 {
out.write_string("{}")
} else {
out.write_string("{")
for i = 0; i < table.entries.length(); i = i + 1 {
if i > 0 {
out.write_string(",")
}
out.write_string(" ")
out.write_string(render_key(table.entries[i].0))
out.write_string(" = ")
out.write_string(render_value(table.entries[i].1))
}
out.write_string(" }")
}
out.to_string()
}
}
}
///|
/// True for a non-empty array whose elements are all tables, the shape written
/// with `[[name]]` headers.
fn is_array_of_tables(value : TomlValue) -> Bool {
match value {
TArray(items) =>
if items.length() == 0 {
false
} else {
let mut all_tables = true
for i = 0; i < items.length(); i = i + 1 {
match items[i] {
TTable(_) => ()
_ => all_tables = false
}
}
all_tables
}
_ => false
}
}
///|
fn render_header_path(path : Array[String]) -> String {
let out = StringBuilder()
for i = 0; i < path.length(); i = i + 1 {
if i > 0 {
out.write_string(".")
}
out.write_string(render_key(path[i]))
}
out.to_string()
}
///|
fn write_pair(out : StringBuilder, key : String, value : TomlValue) -> Unit {
out.write_string(render_key(key))
out.write_string(" = ")
out.write_string(render_value(value))
out.write_string("\n")
}
///|
/// Plain values are written before sub-tables at every level, so that a
/// `[a.b]` header can never capture a later sibling key.
fn write_table(out : StringBuilder, table : Toml, path : Array[String]) -> Unit {
for i = 0; i < table.entries.length(); i = i + 1 {
let key = table.entries[i].0
let value = table.entries[i].1
let is_nested = match value {
TTable(_) => true
_ => is_array_of_tables(value)
}
if !is_nested {
write_pair(out, key, value)
}
}
for i = 0; i < table.entries.length(); i = i + 1 {
let key = table.entries[i].0
let value = table.entries[i].1
let child = push_path(path, key)
match value {
TTable(sub) => {
// The blank line separates a header from whatever came before it; at
// the very start of the document there is nothing to separate.
write_separator(out)
out.write_string("[")
out.write_string(render_header_path(child))
out.write_string("]\n")
write_table(out, sub, child)
}
TArray(items) =>
if is_array_of_tables(value) {
for j = 0; j < items.length(); j = j + 1 {
write_separator(out)
out.write_string("[[")
out.write_string(render_header_path(child))
out.write_string("]]\n")
match items[j] {
TTable(sub) => write_table(out, sub, child)
_ => ()
}
}
}
_ => ()
}
}
}
///|
/// Write the blank line that precedes a table header, unless the document is
/// still empty.
fn write_separator(out : StringBuilder) -> Unit {
if !out.is_empty() {
out.write_string("\n")
}
}
///|
/// Canonical re-serialization.
///
/// The result re-parses to an equal `Toml`. Within a table, plain values are
/// written before sub-tables so that TOML's header scoping cannot move a key
/// into the wrong table; sub-tables and arrays of tables keep their relative
/// order.
pub fn Toml::to_string(self : Toml) -> String {
let out = StringBuilder()
write_table(out, self, [])
out.to_string()
}