use crate::common::{Doc, Ident, Literal, Path};
use alloc::boxed::Box;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Module {
pub doc: Doc,
pub future: Vec<Ident>,
pub imports: Vec<Import>,
pub body: Vec<Statement>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Import {
Module {
path: Path,
alias: Option<Ident>,
},
From {
path: Path,
names: Vec<ImportName>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImportName {
pub name: Ident,
pub alias: Option<Ident>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Statement {
Class(Class),
Annotated(Annotated),
Assign(Assign),
Function(Function),
Set {
place: Expr,
value: Expr,
},
Expr(Expr),
If {
condition: Expr,
then: Vec<Statement>,
otherwise: Option<Vec<Statement>>,
},
For {
counter: Ident,
bound: Expr,
body: Vec<Statement>,
},
While {
condition: Expr,
body: Vec<Statement>,
},
Break,
Return(Option<Expr>),
Comment(Doc),
Pass,
Verbatim(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Function {
pub doc: Doc,
pub decorators: Vec<Decorator>,
pub name: Ident,
pub params: Vec<Param>,
pub returns: Option<Type>,
pub body: Vec<Statement>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Param {
pub name: Ident,
pub type_: Option<Type>,
pub default: Option<Expr>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Annotated {
pub doc: Doc,
pub name: Ident,
pub type_: Type,
pub value: Option<Expr>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Assign {
pub doc: Doc,
pub name: Ident,
pub value: Expr,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Class {
pub doc: Doc,
pub decorators: Vec<Decorator>,
pub name: Ident,
pub bases: Vec<Type>,
pub body: Vec<Statement>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Decorator {
pub path: Path,
pub args: Vec<Arg>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Arg {
Positional(Expr),
Keyword(Keyword),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Keyword {
pub name: Ident,
pub value: Expr,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Type {
Name(Path),
Subscript {
base: Path,
args: Vec<Type>,
},
Optional(Box<Type>),
Annotated {
base: Box<Type>,
markers: Vec<Expr>,
},
}
impl Type {
#[must_use]
pub fn name(text: &str) -> Self {
Self::Name(Path::single(text))
}
#[must_use]
pub fn subscript(base: Path, args: Vec<Self>) -> Self {
Self::Subscript { base, args }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Expr {
Literal(Literal),
Path(Path),
Call {
func: Path,
args: Vec<Arg>,
},
Tuple(Vec<Expr>),
List(Vec<Expr>),
Dict(Vec<(Expr, Expr)>),
Attribute {
value: Box<Expr>,
name: Ident,
},
Index {
value: Box<Expr>,
index: Box<Expr>,
},
Slice {
start: Option<Box<Expr>>,
end: Option<Box<Expr>>,
},
Unary {
op: &'static str,
value: Box<Expr>,
},
Paren(Box<Expr>),
Binary {
op: &'static str,
lhs: Box<Expr>,
rhs: Box<Expr>,
},
Annotation(Box<Type>),
}
impl Expr {
#[must_use]
pub fn name(text: &str) -> Self {
Self::Path(Path::single(text))
}
#[must_use]
pub fn int(value: i128) -> Self {
Self::Literal(Literal::int(value))
}
}
#[must_use]
pub fn escape_name(name: &str) -> String {
if RESERVED.contains(&name) {
let mut out = name.to_string();
out.push('_');
return out;
}
name.to_string()
}
pub const RESERVED: &[&str] = &[
"False",
"None",
"True",
"and",
"as",
"assert",
"async",
"await",
"break",
"class",
"continue",
"def",
"del",
"elif",
"else",
"except",
"finally",
"for",
"from",
"global",
"if",
"import",
"in",
"is",
"lambda",
"nonlocal",
"not",
"or",
"pass",
"raise",
"return",
"try",
"while",
"with",
"yield",
"case",
"match",
"type",
"annotations",
"dataclasses",
"enum",
"typing",
"mro",
"bool",
"bytes",
"float",
"int",
"list",
"object",
"str",
"tuple",
];
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use super::*;
#[test]
fn a_keyword_escapes_with_a_trailing_underscore() {
assert_eq!(escape_name("class"), "class_");
assert_eq!(escape_name("lambda"), "lambda_");
assert_eq!(escape_name("payload"), "payload");
}
#[test]
fn a_soft_keyword_escapes_although_it_would_parse() {
for name in ["match", "case", "type"] {
assert_eq!(escape_name(name), alloc::format!("{name}_"));
}
}
#[test]
fn a_name_the_generated_module_binds_escapes_too() {
for name in ["int", "list", "tuple", "str", "typing", "dataclasses"] {
assert_eq!(escape_name(name), alloc::format!("{name}_"));
}
}
#[test]
fn the_one_name_an_enumeration_may_not_use_escapes() {
assert_eq!(escape_name("mro"), "mro_");
assert_eq!(escape_name("name"), "name");
assert_eq!(escape_name("value"), "value");
}
#[test]
fn a_leading_underscore_is_left_alone() {
assert_eq!(escape_name("_reserved"), "_reserved");
assert_eq!(escape_name("_d"), "_d");
}
#[test]
fn the_reserved_list_has_no_duplicates() {
let mut seen: Vec<&str> = Vec::new();
for name in RESERVED {
assert!(!seen.contains(name), "{name} is listed twice");
seen.push(name);
}
}
#[test]
fn an_annotated_type_carries_its_metadata_beside_the_type_a_checker_sees() {
let type_ = Type::Annotated {
base: Box::new(Type::name("int")),
markers: alloc::vec![Expr::Call {
func: Path::single("IdlInt"),
args: alloc::vec![Arg::Keyword(Keyword {
name: Ident::new("bits"),
value: Expr::int(32),
})],
}],
};
let Type::Annotated { base, markers } = &type_ else {
panic!("not annotated");
};
assert_eq!(**base, Type::name("int"));
assert_eq!(markers.len(), 1);
}
}