use crate::common::{Doc, Ident, Literal};
use alloc::boxed::Box;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct File {
pub doc: Doc,
pub body: Vec<Stat>,
pub returns: Expr,
pub end_text: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Stat {
Local {
doc: Doc,
name: Ident,
value: Option<Expr>,
},
Assign {
doc: Doc,
target: Place,
value: Expr,
},
Function(Function),
Return(Option<Expr>),
Comment(Doc),
Annotation(Doc),
Verbatim(String),
Perform(Expr),
If {
condition: Expr,
then: Vec<Stat>,
otherwise: Option<Vec<Stat>>,
},
While {
condition: Expr,
body: Vec<Stat>,
},
NumericFor {
counter: Ident,
from: Expr,
to: Expr,
body: Vec<Stat>,
},
Break,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Function {
pub doc: Doc,
pub target: Place,
pub params: Vec<Ident>,
pub body: Vec<Stat>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Place {
Name(Ident),
Field {
base: Box<Place>,
name: Ident,
},
Index {
base: Box<Place>,
key: Box<Expr>,
},
Paren(Box<Expr>),
}
impl Place {
#[must_use]
pub fn name(text: &str) -> Self {
Self::Name(Ident::new(text))
}
#[must_use]
pub fn field(self, name: &str) -> Self {
if is_identifier(name) {
return Self::Field {
base: Box::new(self),
name: Ident::new(name),
};
}
Self::Index {
base: Box::new(self),
key: Box::new(Expr::Literal(Literal::string(name))),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Expr {
Nil,
Literal(Literal),
Place(Place),
Table(Table),
Call {
func: Box<Place>,
args: Vec<Expr>,
},
Binary {
op: &'static str,
lhs: Box<Expr>,
rhs: Box<Expr>,
},
Unary {
op: &'static str,
value: Box<Expr>,
},
Paren(Box<Expr>),
}
impl Expr {
#[must_use]
pub fn int(value: i128) -> Self {
Self::Literal(Literal::int(value))
}
#[must_use]
pub fn string(text: &str) -> Self {
Self::Literal(Literal::string(text))
}
#[must_use]
pub const fn bool(value: bool) -> Self {
Self::Literal(Literal::Bool(value))
}
#[must_use]
pub const fn place(place: Place) -> Self {
Self::Place(place)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Table {
pub entries: Vec<Entry>,
}
impl Table {
#[must_use]
pub const fn of(entries: Vec<Entry>) -> Self {
Self { entries }
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Entry {
pub doc: Doc,
pub key: Key,
pub value: Expr,
}
impl Entry {
#[must_use]
pub fn named(key: &str, value: Expr) -> Self {
Self {
doc: Doc::none(),
key: table_key(key),
value,
}
}
#[must_use]
pub fn positional(value: Expr) -> Self {
Self {
doc: Doc::none(),
key: Key::Positional,
value,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Key {
Positional,
Name(Ident),
Expr(Expr),
}
#[must_use]
pub fn table_key(name: &str) -> Key {
if is_identifier(name) {
return Key::Name(Ident::new(name));
}
Key::Expr(Expr::Literal(Literal::string(name)))
}
#[must_use]
pub fn is_identifier(name: &str) -> bool {
let mut chars = name.chars();
let Some(first) = chars.next() else {
return false;
};
if !(first.is_ascii_alphabetic() || first == '_') {
return false;
}
if !chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_') {
return false;
}
!KEYWORDS.contains(&name)
}
pub const KEYWORDS: &[&str] = &[
"and", "break", "do", "else", "elseif", "end", "false", "for", "function", "goto", "if", "in",
"local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while",
];
#[must_use]
pub fn float_text(text: &str) -> String {
let looks_float = text
.chars()
.any(|ch| ch == '.' || ch == 'e' || ch == 'E' || ch == 'n' || ch == 'i');
if looks_float {
return text.to_string();
}
let mut out = text.to_string();
out.push_str(".0");
out
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use super::*;
use alloc::vec;
#[test]
fn a_keyword_is_not_written_after_a_dot() {
assert!(!is_identifier("end"));
assert!(!is_identifier("local"));
assert!(!is_identifier("nil"));
assert!(is_identifier("payload"));
assert!(is_identifier("_d"));
}
#[test]
fn a_name_that_is_not_an_identifier_is_not_written_after_a_dot() {
assert!(!is_identifier(""));
assert!(!is_identifier("2fast"));
assert!(!is_identifier("has space"));
assert!(!is_identifier("a-b"));
}
#[test]
fn a_keyword_keeps_its_name_and_changes_only_its_form() {
match table_key("end") {
Key::Expr(Expr::Literal(Literal::Str(text))) => assert_eq!(text, "end"),
other => panic!("{other:?} is not the bracket form"),
}
match table_key("x") {
Key::Name(name) => assert_eq!(name.as_str(), "x"),
other => panic!("{other:?} is not the dotted form"),
}
}
#[test]
fn a_place_takes_the_bracket_form_for_a_keyword_and_the_dot_form_otherwise() {
let dotted = Place::name("M").field("Pose");
assert!(matches!(dotted, Place::Field { .. }));
let bracketed = Place::name("M").field("end");
assert!(matches!(bracketed, Place::Index { .. }));
}
#[test]
fn the_keyword_list_has_no_duplicates_and_holds_goto() {
let mut seen: Vec<&str> = Vec::new();
for name in KEYWORDS {
assert!(!seen.contains(name), "{name} is listed twice");
seen.push(name);
}
assert!(KEYWORDS.contains(&"goto"));
assert_eq!(KEYWORDS.len(), 22);
}
#[test]
fn a_float_literal_is_written_so_that_math_type_answers_float() {
assert_eq!(float_text("0"), "0.0");
assert_eq!(float_text("-3"), "-3.0");
assert_eq!(float_text("1.5"), "1.5");
assert_eq!(float_text("1e10"), "1e10");
assert_eq!(float_text("1E10"), "1E10");
}
#[test]
fn a_table_keeps_its_entries_in_the_order_they_were_added() {
let table = Table::of(vec![
Entry::positional(Expr::string("first")),
Entry::positional(Expr::string("second")),
]);
assert_eq!(table.entries.len(), 2);
assert_eq!(table.entries[0].value, Expr::string("first"));
assert_eq!(table.entries[1].value, Expr::string("second"));
}
#[test]
fn an_empty_table_is_representable() {
assert!(Table::default().is_empty());
}
}