//! Corresponds to `ast::lit`. use chumsky::prelude::*; use crate::ast::{ Expr, Ident, Lit, NumLit, NumLitStr, Space, StringLit, StringLitElem, TableLit, TableLitElem, }; use crate::builtin::Builtin; use super::basic::{EParser, Error}; fn builtin_lit() -> impl Parser { just('\'').ignore_then(choice(( text::keyword("get").to(Builtin::Get), text::keyword("set").to(Builtin::Set), text::keyword("getraw").to(Builtin::GetRaw), text::keyword("setraw").to(Builtin::SetRaw), text::keyword("getmeta").to(Builtin::GetMeta), text::keyword("setmeta").to(Builtin::SetMeta), text::keyword("scope").to(Builtin::Scope), text::keyword("arg").to(Builtin::Arg), text::keyword("destructure").to(Builtin::Destructure), ))) } fn num_lit_str_radix(radix: u32) -> impl Parser { // Minimum amount of digits required to represent i64::MAX. The rest of this // code assumes that any value that can be represented using this amount of // digits fits into an u64. let max_digits = match radix { 2 => 63, 10 => 19, 16 => 16, _ => panic!("unsupported radix"), }; // Representations of i64::MAX. let max_value = match radix { 2 => "0b_1111111_11111111_11111111_11111111_11111111_11111111_11111111_11111111", 10 => "9_223_372_036_854_775_807", 16 => "0x_7fff_ffff_ffff_ffff", _ => panic!("unsupported radix"), }; let constructor = match radix { 2 => NumLitStr::Bin, 10 => NumLitStr::Dec, 16 => NumLitStr::Hex, _ => panic!("unsupported radix"), }; filter(move |c: &char| c.is_digit(radix) || *c == '_') .repeated() .at_least(1) .collect::() .try_map(move |s, span| { let digits = s.chars().filter(|c| *c != '_').collect::(); if digits.is_empty() { let msg = "integer literal needs to contain at least one digit"; return Err(Simple::custom(span, msg)); } else if digits.len() > max_digits { let msg = format!("integer literal too large, the maximum value is {max_value}"); return Err(Simple::custom(span, msg)); } let value = u64::from_str_radix(&digits, radix).unwrap(); if value <= i64::MAX as u64 { Ok((value as i64, constructor(s))) } else { let msg = format!("integer literal too large, the maximum value is {max_value}"); Err(Simple::custom(span, msg)) } }) } fn num_lit() -> impl Parser { (just("0b").ignore_then(num_lit_str_radix(2))) .or(just("0x").ignore_then(num_lit_str_radix(16))) .or(num_lit_str_radix(10)) .map_with_span(|(value, str), span| NumLit { value, str, span }) } fn string_lit_elem() -> impl Parser { let plain = filter(|c: &char| !matches!(c, '\\' | '"' | '\t' | '\r' | '\n')) .repeated() .at_least(1) .collect::() .map(StringLitElem::Plain); // The maximum unicode codepoint is 10ffff, which has 6 digits. let unicode_char = filter(|c: &char| c.is_ascii_hexdigit()) .repeated() .at_least(1) .at_most(6) .collect::() .try_map(|str, span| { let msg = "not a valid unicode code point"; let n = u32::from_str_radix(&str, 16).unwrap(); let c: char = n.try_into().map_err(|_| Simple::custom(span, msg))?; Ok(c) }); let unicode = just("\\u{") .ignore_then(unicode_char) .then_ignore(just('}')) .map(StringLitElem::Unicode); let backslash = just("\\\\").to(StringLitElem::Backslash); let double_quote = just("\\\"").to(StringLitElem::DoubleQuote); let tab = just("\\t").to(StringLitElem::Tab); let carriage_return = just("\\r").to(StringLitElem::CarriageReturn); let newline = just("\\n").to(StringLitElem::Newline); plain .or(unicode) .or(backslash) .or(double_quote) .or(tab) .or(carriage_return) .or(newline) } fn string_lit() -> impl Parser { string_lit_elem() .repeated() .delimited_by(just('"'), just('"')) .map_with_span(|elems, span| StringLit { elems, span }) } pub fn table_lit_elem( space: EParser, ident: EParser, expr: EParser, ) -> EParser { let positional = expr .clone() .map(|value| TableLitElem::Positional(Box::new(value))); let named = ident .then(space.clone()) .then_ignore(just(':')) .then(space) .then(expr) .map_with_span(|(((name, s0), s1), value), span| TableLitElem::Named { name, s0, s1, value: Box::new(value), span, }); named.or(positional).boxed() } fn table_lit( space: EParser, table_lit_elem: EParser, ) -> impl Parser { let elem = space .clone() .then(table_lit_elem) .then(space.clone()) .map(|((s0, elem), s1)| (s0, elem, s1)); let trailing_comma = just(',').ignore_then(space).or_not(); let elems = elem.separated_by(just(',')).then(trailing_comma); just("'{") .ignore_then(elems) .then_ignore(just('}')) .map_with_span(|(elems, trailing_comma), span| TableLit { elems, trailing_comma, span, }) } pub fn lit(space: EParser, table_lit_elem: EParser) -> EParser { let nil = text::keyword("nil").map_with_span(|_, span| Lit::Nil(span)); let r#true = text::keyword("true").map_with_span(|_, span| Lit::Bool(true, span)); let r#false = text::keyword("false").map_with_span(|_, span| Lit::Bool(false, span)); let builtin = builtin_lit().map_with_span(Lit::Builtin); let num = num_lit().map(Lit::Num); let string = string_lit().map(Lit::String); let table = table_lit(space, table_lit_elem).map(Lit::Table); nil.or(r#true) .or(r#false) .or(builtin) .or(num) .or(string) .or(table) .boxed() }