plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
plum-core/tests/parser_test.rs
#![allow(non_snake_case)]
use plum_core::ast::*;
use plum_core::AstParser;
fn parse(src: &str) -> Source {
let mut parser = tree_sitter::Parser::new();
parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap();
let tree = parser.parse(src, None).unwrap();
assert!(!tree.root_node().has_error(), "parse error:\n{}", tree.root_node().to_sexp());
let ap = AstParser::new(src);
ap.parseSource(tree.root_node())
}
fn onlyFn(source: &Source) -> &Fn {
source.items.iter().find_map(|i| match i { Item::Fn(f) => Some(f), _ => None }).expect("expected a Fn item")
}
fn onlyTrait(source: &Source) -> &Trait {
source.items.iter().find_map(|i| match i { Item::Trait(t) => Some(t), _ => None }).expect("expected a Trait item")
}
fn onlyEnum(source: &Source) -> &Enum {
source.items.iter().find_map(|i| match i { Item::Enum(e) => Some(e), _ => None }).expect("expected an Enum item")
}
#[test]
fn nestedClassMethodsBecomeTopLevelFnItemsWithTypeParamSet() {
let src = "\
type Cat =
name: Str
fun getName(self) -> Str =
self.name
fun setName(self, n: Str) =
todo
";
let source = parse(src);
assert_eq!(source.items.len(), 3, "class + 2 nested methods, in order");
let Item::Class(class) = &source.items[0] else { panic!("expected a Class item first") };
assert_eq!(class.name, "Cat");
let Item::Fn(get_name) = &source.items[1] else { panic!("expected getName immediately after the class") };
assert_eq!(get_name.name, "getName");
assert_eq!(get_name.type_param, Some("Cat".to_string()));
let Item::Fn(set_name) = &source.items[2] else { panic!("expected setName immediately after getName") };
assert_eq!(set_name.name, "setName");
assert_eq!(set_name.type_param, Some("Cat".to_string()));
}
#[test]
fn nestedEnumMethodBecomesTopLevelFnItemWithTypeParamSet() {
let src = "\
enum Step =
| ReadMin
| ReadMax
fun toNumber(self) -> Int =
match self
ReadMin => 0
ReadMax => 1
";
let source = parse(src);
assert_eq!(source.items.len(), 2, "enum + 1 nested method, in order");
let Item::Enum(e) = &source.items[0] else { panic!("expected an Enum item first") };
assert_eq!(e.name, "Step");
let Item::Fn(to_number) = &source.items[1] else { panic!("expected toNumber immediately after the enum") };
assert_eq!(to_number.name, "toNumber");
assert_eq!(to_number.type_param, Some("Step".to_string()));
}
#[test]
fn enumDiscriminantValuesParseIntoParamsAndVariantValues() {
let src = "\
enum Step(n: Int) =
| ReadMin(0)
| ReadMax(1)
";
let source = parse(src);
let e = onlyEnum(&source);
assert_eq!(e.params, vec![EnumParam { name: "n".to_string(), ty: Type { name: "Int".to_string(), generics: vec![] } }]);
assert_eq!(e.variants[0].fields, Vec::<String>::new());
assert_eq!(e.variants[0].values, vec![Expr::Int(0)]);
assert_eq!(e.variants[1].fields, Vec::<String>::new());
assert_eq!(e.variants[1].values, vec![Expr::Int(1)]);
}
#[test]
fn classAndEnumWithNoNestedMethodsProduceNoExtraFnItems() {
let src = "\
type Dog =
name: Str
enum Bool =
| True
| False
";
let source = parse(src);
assert_eq!(source.items.len(), 2, "no nested methods means no extra Item::Fn entries");
assert!(matches!(source.items[0], Item::Class(_)));
assert!(matches!(source.items[1], Item::Enum(_)));
}
#[test]
fn closureLiteralParsesWithParamsAndBody() {
let src = "\
fun useClosure() -> Bool =
cb = |v|
True
cb(5)
";
let source = parse(src);
let f = onlyFn(&source);
let FnBody::Block(block) = &f.body else { panic!("expected a block body") };
let Stmt::Assign(assign) = &block.stmts[0] else { panic!("expected an assign statement") };
let Expr::Closure(closure) = &assign.values[0] else { panic!("expected a closure expression, got {:?}", assign.values[0]) };
assert_eq!(closure.params, vec!["v".to_string()]);
assert_eq!(closure.body.stmts.len(), 1);
}
#[test]
fn closureLiteralParsesWithNoParams() {
let src = "\
fun useClosure() -> Bool =
cb = ||
True
cb()
";
let source = parse(src);
let f = onlyFn(&source);
let FnBody::Block(block) = &f.body else { panic!("expected a block body") };
let Stmt::Assign(assign) = &block.stmts[0] else { panic!("expected an assign statement") };
let Expr::Closure(closure) = &assign.values[0] else { panic!("expected a closure expression") };
assert!(closure.params.is_empty());
}
#[test]
fn fnWithoutReceiverButWithReturnTypeHasNoTypeParam() {
let src = "fun main() -> Int =\n 0\n";
let source = parse(src);
let f = onlyFn(&source);
assert_eq!(f.type_param, None, "return type must not be mistaken for a method receiver");
assert_eq!(f.returns.as_ref().map(|t| t.name.clone()), Some("Int".to_string()));
}
#[test]
fn nestedMethodWithReturnTypeHasCorrectTypeParam() {
let src = "type Cat =\n name: Str\n\n fun toStr() -> Str =\n \"x\"\n";
let source = parse(src);
let f = onlyFn(&source);
assert_eq!(f.type_param, Some("Cat".to_string()));
assert_eq!(f.returns.as_ref().map(|t| t.name.clone()), Some("Str".to_string()));
}
#[test]
fn fnValueTypeParamParsesWithPositionalTypesAndReturn() {
let src = "fun each(cb: fn(Int) -> Bool) -> Bool =\n True\n";
let source = parse(src);
let f = onlyFn(&source);
let ParamType::Fn(param_types, ret) = &f.params[0].ty else { panic!("expected ParamType::Fn, got {:?}", f.params[0].ty) };
assert_eq!(param_types.len(), 1);
assert_eq!(param_types[0].name, "Int");
assert_eq!(ret.as_ref().map(|t| t.name.clone()), Some("Bool".to_string()));
}
#[test]
fn traitMethodReturnTypeIsNotMangledToArrowToken() {
let src = "trait Show =\n show() -> Str\n";
let source = parse(src);
let t = onlyTrait(&source);
let method = &t.methods[0];
assert_eq!(method.name, "show");
assert_eq!(
method.returns.as_ref().map(|r| r.name.clone()),
Some("Str".to_string()),
"trait method return type must be the real type, not the '->' token"
);
}
#[test]
fn fnValueTypeParamParsesWithNoReturn() {
let src = "fun each(cb: fn(Int)) -> Bool =\n True\n";
let source = parse(src);
let f = onlyFn(&source);
let ParamType::Fn(param_types, ret) = &f.params[0].ty else { panic!("expected ParamType::Fn") };
assert_eq!(param_types.len(), 1);
assert!(ret.is_none());
}