ltk_ritobin 0.11.0

Ritobin text format parser and writer for League Toolkit
Documentation
//! Ritobin parser, tokenizer & other relevant types.
//!
//! To actually parse text, see [`crate::Cst::parse`].
//!
//! Parsing is purely syntactic: it builds the CST without performing any semantic
//! analysis such as type resolution or validation beyond what is needed to form a valid CST.
//!
//! See [`crate::Cst::build_bin`] & [`crate::typecheck`] for further typechecking/bin construction.

mod error;
pub use error::*;

mod parser;
pub use parser::*;

pub mod tokenizer;
pub use tokenizer::{Token, TokenKind};

pub mod impls;

#[deprecated(note = "use ltk_ritobin::Span instead")]
pub use crate::Span;

use crate::cst;

#[cfg(test)]
mod test {

    use crate::{
        cst::{Cst, Kind},
        print::CstPrinter,
    };

    fn assert_success(text: &str) -> Cst {
        let cst = Cst::parse(text);

        let mut buf = String::new();
        cst.print(&mut buf, text);

        println!("{buf}");

        assert!(cst.errors.is_empty(), "Parse errors: {:#?}", cst.errors);
        assert_spans_nested(&cst);
        cst
    }

    /// Asserts that every node's span covers the spans of all of its children.
    fn assert_spans_nested(cst: &Cst) {
        for node in &cst.nodes {
            for child in node.children.get(cst) {
                let span = child.span(cst);
                assert!(
                    node.span.start <= span.start && span.end <= node.span.end,
                    "{:?} ({}..{}) does not cover its child ({}..{})",
                    node.kind,
                    node.span.start,
                    node.span.end,
                    span.start,
                    span.end,
                );
            }
        }
    }

    #[allow(unused, reason = "tests will use this")]
    fn assert_fail(text: &str) {
        let cst = Cst::parse(text);

        let mut buf = String::new();
        cst.print(&mut buf, text);

        println!("{buf}");
        assert!(!cst.errors.is_empty(), "Parsed successfully",);
    }

    #[test]
    fn eof() {
        assert_fail("0:[?");
        assert_fail("a = ?");
        assert_fail("a:");
    }

    #[test]
    fn error_spans_stay_within_the_source() {
        // all of these error at the end of input, where the "point just past
        // the token" error span used to run past the source
        for text in ["entries: map[hash,embed] = {", "a: u32 =", "a:", ""] {
            let cst = Cst::parse(text);
            for err in &cst.errors {
                assert!(
                    err.span.start <= err.span.end,
                    "inverted error span {:?} for {text:?}",
                    err.span
                );
                assert!(
                    err.span.end as usize <= text.len(),
                    "error span {:?} leaves the source (len {}) for {text:?}",
                    err.span,
                    text.len()
                );
            }
        }
    }

    #[test]
    fn comments() {
        assert_success(
            r#"
#PROP_text
type: string = "my_str" # inline comment
version: u32 = 3
linked: list[string] = { }
entries: map[hash, embed] = {
    "foo" = Bar {
        a = 5 # 23
        # asdasd
    }
}
        "#,
        );
    }

    #[test]
    fn inline_block_spans() {
        let text = "a: list[pointer] = { B { x: f32 = 1 } }";
        let cst = assert_success(text);

        let block = class_block(&cst).expect("class block");
        assert_eq!(&text[block.span], "{ x: f32 = 1 }");
    }

    #[test]
    fn nested_inline_block_spans() {
        let text = r#"loadscreen: embed = CensoredImage {
    image: embed = Image { src: string = "val" }
}"#;
        let cst = assert_success(text);

        let block = class_block(&cst).expect("class block");
        assert_eq!(
            &text[block.span],
            "{\n    image: embed = Image { src: string = \"val\" }\n}"
        );
    }

    /// The [`Kind::Block`] of the first [`Kind::Class`] in the tree.
    fn class_block(cst: &Cst) -> Option<&crate::cst::Node> {
        let class = cst.nodes.iter().find(|n| n.kind == Kind::Class)?;
        class
            .children
            .get(cst)
            .iter()
            .find_map(|c| c.tree(cst))
            .filter(|n| n.kind == Kind::Block)
    }

    #[test]
    fn inline_comment_into_eof() {
        assert_success(r#"mVelMultiplier: f32 = 0 # asd"#);
        assert_fail(r#"mVelMultiplier: f32 = # asd"#);
    }

    #[ignore = "Nice to have"]
    #[test]
    fn naked_class() {
        let text = r#"
#PROP_text
type: string = "PROP"
version: u32 = 3
linked: list[string] = { }
        entries: map[hash, embed] = {
            "thing" = ClassThing { ooo }
        }
        "#;
        let cst = assert_success(text);

        let partial = cst.build_bin(text);
        assert!(
            !partial.diagnostics.is_empty(),
            "There should be an error for the naked 'ooo' in the class block"
        );
    }

    #[test]
    fn smoke_test() {
        let text = r#"
#PROP_text
type: string = "PROP"
version: u32 = 3
linked: list[string] = { }
entries: map[hash, embed] = {
    "foo" = Foo {
        guy: u32 = "asdasd"
    }
}

"#;
        let cst = Cst::parse(text);

        let mut str = String::new();
        cst.print(&mut str, text);
        eprintln!("text len: {}", text.len());
        eprintln!("{str}\n====== errors: ======\n");

        let errors = &cst.errors;
        for err in errors {
            eprintln!("{:?}: {:#?}", &text[err.span], err.kind);
        }

        assert!(errors.is_empty());

        let ast = cst.build_ast(&str);
        let tree = ast.to_bin(&str);

        eprintln!("{str}\n====== type errors: ======\n");
        for err in errors {
            eprintln!("{:?}: {:#?}", &text[err.span], err);
        }

        eprintln!("==== FINAL TREE =====\n{tree:#?}");
    }

    #[test]
    fn writer_test() {
        let text = r#"
#PROP_text
type: string = "PROP"
version: u32 = 3
linked: list[string] = { }
entries: map[hash,embed] = {
    "myPath" = VfxEmitter {
        a: string = "hello"
        b: list[i8] = {3 6 1}
    }
    "cock" = VfxEmitterDefinitionData {
                rate: embed = ValueFloat {
                    constantValue: f32 = 1
                }
                particleLifetime: embed = ValueFloat {
                    constantValue: f32 = 1
                }
                particleLinger: option[f32] = {
                    2
                }
                lifetime: option[f32] = {
                    1
                }
                emitterName: string = "JudgementCut"
                bindWeight: embed = ValueFloat {
                    constantValue: f32 = 1
                }
                primitive: pointer = VfxPrimitiveMesh {
                    mMesh: embed = VfxMeshDefinitionData {
                        mMeshName: string = "ASSETS/Characters/viego/Skins/base/judgementcut.skn"
                        mMeshSkeletonName: string = "ASSETS/Characters/viego/Skins/base/judgementcut.skl"
                        mAnimationName: string = "ASSETS/Characters/viego/Skins/base/judgementcut.anm"
                    }
                }
                birthScale0: embed = ValueVector3 {
                    constantValue: vec3 = { 15, 15, 15 }
                }
                blendMode: u8 = 1
                disableBackfaceCull: bool = true
                miscRenderFlags: u8 = 1
                texture: string = "ASSETS/Characters/viego/Skins/base/slashes.dds"
                particleUVScrollRate: embed = IntegratedValueVector2 {
                    constantValue: vec2 = { 1, 0 }
                    dynamics: pointer = VfxAnimatedVector2fVariableData {
                        times: list[f32] = {
                            0
                        }
                        values: list[vec2] = {
                            { 1, 0 }
                        }
                    }
                }
            }
}
"#;
        let cst = Cst::parse(text);

        let mut str = String::new();
        cst.print(&mut str, text);

        println!("============= CST ===========");
        println!("{str}");

        let mut str = String::new();
        CstPrinter::new(text, &mut str, Default::default())
            .print(&cst)
            .unwrap();

        println!("{}", "*".repeat(80));
        println!("========\n{str}");
    }
}