// Copyright 2026 moonbit-toml contributors
//
// SPDX-License-Identifier: MIT
///|
/// Parses a value that starts with a digit, `+`, `-`, `i` (inf) or `n` (nan):
/// integers, floats, and the four TOML datetime forms.
fn Scanner::parse_number_or_datetime(self : Scanner) -> Value raise ParseError {
let start = self.mark()
if self.looks_like_date() {
return self.parse_datetime_value(start)
}
if self.looks_like_time() {
let time = self.parse_time_value(start, require_seconds=false)
return Value::Datetime({
date: None,
time: Some(time),
offset: 0,
has_offset: false,
})
}
self.parse_number(start)
}
///|
/// True if the input at the current position matches the shape
/// `DDDD-DD-DD` of a TOML date.
fn Scanner::looks_like_date(self : Scanner) -> Bool {
for i in [0, 1, 2, 3, 5, 6, 8, 9] {
let ok = match self.peek_at(i) {
Some(c) => is_digit(c)
None => false
}
if !ok {
return false
}
}
self.peek_at(4) == Some('-') && self.peek_at(7) == Some('-')
}
///|
/// True if the input at the current position matches the shape `DD:` of a
/// TOML local time (which cannot be a valid integer literal).
fn Scanner::looks_like_time(self : Scanner) -> Bool {
let d1 = match self.peek_at(0) {
Some(c) => is_digit(c)
None => false
}
let d2 = match self.peek_at(1) {
Some(c) => is_digit(c)
None => false
}
d1 && d2 && self.peek_at(2) == Some(':')
}
///|
/// Parses `full-date [ time-delim partial-time [ time-offset ] ]`.
/// The position is at the start of the date.
fn Scanner::parse_datetime_value(
self : Scanner,
start : Mark,
) -> Value raise ParseError {
let date = self.parse_date_value(start)
let delim = self.peek()
if delim == Some('T') || delim == Some('t') {
ignore(self.bump())
// Seconds are required in date-times (they may only be omitted in
// time-only values).
let time = self.parse_time_value(start, require_seconds=true)
if self.at_offset_start() {
let offset = self.parse_offset_value(start)
return Value::Datetime({
date: Some(date),
time: Some(time),
offset,
has_offset: true,
})
}
return Value::Datetime({
date: Some(date),
time: Some(time),
offset: 0,
has_offset: false,
})
}
if delim == Some(' ') || delim == Some('\t') {
// A space may separate date and time, but only spaces/tabs may
// intervene; otherwise the value is just a local date.
let m = self.mark()
self.skip_inline_ws()
if self.looks_like_time() {
let time = self.parse_time_value(start, require_seconds=true)
if self.at_offset_start() {
let offset = self.parse_offset_value(start)
return Value::Datetime({
date: Some(date),
time: Some(time),
offset,
has_offset: true,
})
}
return Value::Datetime({
date: Some(date),
time: Some(time),
offset: 0,
has_offset: false,
})
}
self.reset(m)
}
Value::Datetime({
date: Some(date),
time: None,
offset: 0,
has_offset: false,
})
}
///|
fn Scanner::at_offset_start(self : Scanner) -> Bool {
let c = self.peek()
c == Some('Z') || c == Some('z') || c == Some('+') || c == Some('-')
}
///|
fn Scanner::parse_date_value(
self : Scanner,
start : Mark,
) -> DateParts raise ParseError {
self.reset(start)
let year = self.read_fixed_digits(4, start)
ignore(self.bump()) // '-'
let month = self.read_fixed_digits(2, start)
ignore(self.bump()) // '-'
let day = self.read_fixed_digits(2, start)
if month < 1 || month > 12 || day < 1 || day > 31 {
raise self.error_here(
InvalidDatetime(self.here(), self.text_since(start.pos)),
)
}
{ year, month, day, }
}
///|
/// Parses `partial-time`; the position is at the two-digit hour.
/// Seconds may be omitted only in time-only values (`require_seconds=false`).
fn Scanner::parse_time_value(
self : Scanner,
start : Mark,
require_seconds~ : Bool,
) -> TimeParts raise ParseError {
let hour = self.read_fixed_digits(2, start)
ignore(self.bump()) // ':'
let minute = self.read_fixed_digits(2, start)
let mut second = 0
let mut nanos = 0
if self.bump_if(':') {
second = self.read_fixed_digits(2, start)
if self.bump_if('.') {
let mut digits = 0
let mut value = 0
while self.peek() is Some(c) && is_digit(c) {
ignore(self.bump())
if digits < 9 {
value = value * 10 + hex_value(c)
}
digits += 1
}
if digits == 0 {
raise self.error_here(
InvalidDatetime(self.here(), self.text_since(start.pos)),
)
}
// Pad the stored nanoseconds to 9 digits; precision beyond 1 ns is
// truncated.
let pad = if digits > 9 { 0 } else { 9 - digits }
let mut scale = 1
for _i in 0.. 23 || minute > 59 || second > 60 {
raise self.error_here(
InvalidDatetime(self.here(), self.text_since(start.pos)),
)
}
{ hour, minute, second, nanos, }
}
///|
/// Parses `time-offset` (`Z`, `z` or `+HH:MM` / `-HH:MM`).
fn Scanner::parse_offset_value(
self : Scanner,
start : Mark,
) -> Int raise ParseError {
let c = match self.peek() {
Some(c) => c
None =>
raise self.error_here(
InvalidDatetime(self.here(), self.text_since(start.pos)),
)
}
if c == 'Z' || c == 'z' {
ignore(self.bump())
return 0
}
let negative = c == '-'
ignore(self.bump())
let hour = self.read_fixed_digits(2, start)
if !self.bump_if(':') {
raise self.error_here(
InvalidDatetime(self.here(), self.text_since(start.pos)),
)
}
let minute = self.read_fixed_digits(2, start)
if hour > 23 || minute > 59 {
raise self.error_here(
InvalidDatetime(self.here(), self.text_since(start.pos)),
)
}
let minutes = hour * 60 + minute
if negative {
-minutes
} else {
minutes
}
}
///|
/// Reads exactly `n` decimal digits and returns their numeric value.
fn Scanner::read_fixed_digits(
self : Scanner,
n : Int,
ctx : Mark,
) -> Int raise ParseError {
let mut v = 0
for _i in 0.. c
None =>
raise self.error_here(
InvalidDatetime(self.here(), self.text_since(ctx.pos)),
)
}
if !is_digit(c) {
raise self.error_here(
InvalidDatetime(self.here(), self.text_since(ctx.pos)),
)
}
v = v * 10 + hex_value(c)
ignore(self.bump())
}
v
}
///|
/// Parses an integer or float literal (decimal, hex, octal or binary),
/// including `inf` / `nan`. The position is at the first character.
fn Scanner::parse_number(
self : Scanner,
start : Mark,
) -> Value raise ParseError {
let mut negative = false
let mut signed = false
if self.peek() == Some('+') || self.peek() == Some('-') {
negative = self.peek() == Some('-')
signed = true
ignore(self.bump())
}
if self.peek() == Some('i') {
if self.bump_str("inf") {
return Value::Float(if negative { -1.0 / 0.0 } else { 1.0 / 0.0 })
}
raise self.error_here(InvalidValue(self.here(), 'i'))
}
if self.peek() == Some('n') {
if self.bump_str("nan") {
return Value::Float(0.0 / 0.0)
}
raise self.error_here(InvalidValue(self.here(), 'n'))
}
// Radix integers: unsigned, only without a sign, lowercase marker only.
if !signed && self.peek() == Some('0') {
let second = self.peek_at(1)
let radix = match second {
Some('x') => Some(16)
Some('o') => Some(8)
Some('b') => Some(2)
_ => None
}
match radix {
Some(r) => {
ignore(self.bump()) // '0'
ignore(self.bump()) // marker
let digits = self.scan_digit_run(radix=r)
if digits == "" {
raise self.error_here(
InvalidNumber(self.here(), self.text_since(start.pos)),
)
}
return Value::Int(self.accumulate_radix(digits, r, start))
}
None => ()
}
}
// Decimal integer or float.
let int_part = self.scan_digit_run(radix=10)
if int_part == "" {
raise self.error_here(
InvalidNumber(self.here(), self.text_since(start.pos)),
)
}
if int_part[0] == '0' && int_part.length() > 1 {
raise self.error_here(
InvalidNumber(self.here(), self.text_since(start.pos)),
)
}
let mut is_float = false
let mut frac_part = ""
let mut exp_part = ""
let mut exp_negative = false
if self.peek() == Some('.') {
is_float = true
ignore(self.bump())
frac_part = self.scan_digit_run(radix=10)
if frac_part == "" {
raise self.error_here(
InvalidNumber(self.here(), self.text_since(start.pos)),
)
}
}
if self.peek() == Some('e') || self.peek() == Some('E') {
is_float = true
ignore(self.bump())
if self.peek() == Some('+') || self.peek() == Some('-') {
exp_negative = self.peek() == Some('-')
ignore(self.bump())
}
exp_part = self.scan_digit_run(radix=10)
if exp_part == "" {
raise self.error_here(
InvalidNumber(self.here(), self.text_since(start.pos)),
)
}
}
if !is_float {
return Value::Int(self.accumulate_decimal(int_part, negative, start))
}
// Assemble a canonical decimal float literal and parse it.
let sb = StringBuilder::StringBuilder()
if negative {
sb.write_char('-')
}
sb.write_string(int_part)
if frac_part != "" {
sb.write_char('.')
sb.write_string(frac_part)
} else {
sb.write_string(".0")
}
if exp_part != "" {
sb.write_char('e')
if exp_negative {
sb.write_char('-')
}
sb.write_string(exp_part)
}
let text = sb.to_string()
let d = @string.parse_double(text) catch {
_ => raise self.error_here(InvalidNumber(self.here(), text))
}
Value::Float(d)
}
///|
/// True if `c` is a valid digit in `radix` (16 allows hex letters).
fn is_radix_digit(c : Char, radix : Int) -> Bool {
if radix == 16 {
is_hex_digit(c)
} else {
is_digit(c) && hex_value(c) < radix
}
}
///|
/// Scans a run of digits (in the given radix) separated by single
/// underscores. Returns the cleaned digit text (underscores removed).
/// Raises when the run is empty, starts or ends with an underscore,
/// contains consecutive underscores, or includes a digit out of range.
fn Scanner::scan_digit_run(
self : Scanner,
radix~ : Int,
) -> String raise ParseError {
let sb = StringBuilder::StringBuilder()
let mut got_digit = false
while true {
let c = self.peek()
match c {
Some(c) =>
if is_radix_digit(c, radix) {
ignore(self.bump())
sb.write_char(c)
got_digit = true
} else if c == '_' {
if !got_digit {
raise self.error_here(
InvalidNumber(
self.here(),
"underscore may only appear between digits",
),
)
}
// An underscore must be followed by another digit.
let ok = match self.peek_at(1) {
Some(n) => is_radix_digit(n, radix)
None => false
}
if !ok {
raise self.error_here(
InvalidNumber(
self.here(),
"underscore may only appear between digits",
),
)
}
ignore(self.bump())
got_digit = false
} else {
break
}
None => break
}
}
if !got_digit {
raise self.error_here(
InvalidNumber(self.here(), "expected at least one digit"),
)
}
sb.to_string()
}
///|
/// Accumulates a decimal digit string into a signed 64-bit integer,
/// rejecting values outside the range of Int64.
fn Scanner::accumulate_decimal(
self : Scanner,
digits : String,
negative : Bool,
start : Mark,
) -> Int64 raise ParseError {
let limit : UInt64 = if negative {
9223372036854775808UL
} else {
9223372036854775807UL
}
let mut mag : UInt64 = 0UL
for c in digits {
let d = hex_value(c).to_uint64()
// Check before multiplying: mag * 10 + d must not exceed the limit.
if mag > (limit - d) / 10UL {
raise self.error_here(
InvalidNumber(self.here(), self.text_since(start.pos)),
)
}
mag = mag * 10UL + d
}
let signed_mag = mag.reinterpret_as_int64()
if negative {
-signed_mag
} else {
signed_mag
}
}
///|
/// Accumulates a radix (hex/octal/binary) digit string; the result must fit
/// in a positive 64-bit signed integer.
fn Scanner::accumulate_radix(
self : Scanner,
digits : String,
radix : Int,
start : Mark,
) -> Int64 raise ParseError {
let r : UInt64 = radix.to_uint64()
let max : UInt64 = 9223372036854775807UL
let mut mag : UInt64 = 0UL
for c in digits {
let d = hex_value(c).to_uint64()
if mag > (max - d) / r {
raise self.error_here(
InvalidNumber(self.here(), self.text_since(start.pos)),
)
}
mag = mag * r + d
}
mag.reinterpret_as_int64()
}