microcad-lang-parse 0.5.0

µcad language syntax lexer and parser
Documentation
// Copyright © 2026 The µcad authors <info@microcad.xyz>
// SPDX-License-Identifier: AGPL-3.0-or-later

//! Parser tests

use insta::assert_debug_snapshot;
use microcad_lang_parse::parse;
use test_case::test_case;

#[test_case("single int", "1")]
#[test_case("int overflow", "9999999999999999999")]
#[test_case("single float", "1.2")]
#[test_case("leading dot", ".1")]
#[test_case("trailing dot", "1.")]
#[test_case("exp", ".1e-3; .1e+3; .1e3; 1.e3; 1.2e3; 1e3")]
#[test_case("quantity int", "1mm")]
#[test_case("quantity float", ".2mm")]
#[test_case("basic addition", "1 + 1")]
#[test_case("basic addition, no space", "1+1")]
#[test_case("addition identifier", "length + 1")]
#[test_case("array range", "[1..10]")]
#[test_case("array list", "[1,2,3,4]")]
#[test_case("multiple binary operator", "(1+2)*3+1")]
#[test_case("assignment", "a = b * 2;")]
#[test_case("typed assignment", "a: Length = b * 2mm;")]
#[test_case("block assignment", "a = {a = 1 + 2; a * 3};")]
#[test_case("plain string", r#""plain string""#)]
#[test_case("plain string in expr", r#"a = "plain string int expression" + 1;"#)]
#[test_case("escaped bracket string", r#""string {{ with }} escaped bracket""#)]
#[test_case("escaped quote string", r#""string \" with \" escaped \ quotes""#)]
#[test_case("basic expr string", r#""string {with} expression""#)]
#[test_case("expr string", r#""string {more + complex} expression""#)]
#[test_case(
    "formatted expr string width",
    r#""string {formatted - expression:03} expression""#
)]
#[test_case(
    "formatted expr string accuracy",
    r#""string {formatted - expression:.5} expression""#
)]
#[test_case(
    "formatted expr string both",
    r#""string {formatted - expression:03.5} expression""#
)]
#[test_case("function", "fn foo(a: Length /* test */) -> Length {a * 2}")]
#[test_case("function empty args with comment", "fn foo(/* test */) {2}")]
#[test_case("function with return", "fn foo(a: Length) -> Length {return a * 2;}")]
#[test_case("function with default", "fn foo(a: Length, b = 3mm) {b / a}")]
#[test_case("call empty args", "foo()")]
#[test_case("call empty args comment", "foo(/* foobar */)")]
#[test_case(
    "call positional args",
    "foo(
        1, // foo
        2
    )"
)]
#[test_case("call named args", "foo(a = 1, b = 2)")]
#[test_case("call partially named args", "foo(a, b = 2, 3)")]
#[test_case("comment", "a = 1; // comment")]
#[test_case(
    "multiple comment",
    "// comment1
    // comment2"
)]
#[test_case(
    "multi line comment",
    r#"a = 1; /** multi
    line
    comment
    */
    b = 2;"#
)]
#[test_case(
    "doc comment",
    r#"/// Doc comment
    part Foo() {
        Cylinder(height = 10mm, radius = 5mm);
    }"#
)]
#[test_case(
    "doc comment fn",
    r#"/// Doc comment
    fn Foo() {
        1
    }"#
)]
#[test_case(
    "doc comment const",
    r#"/// Doc comment
    const A = 1;"#
)]
#[test_case(
    "doc comment property",
    r#"/// Doc comment
    prop a = 1;"#
)]
#[test_case(
    "doc comment mod",
    r#"/// Doc comment
    mod foo {
        a = 1;
    }"#
)]
#[test_case(
    "doc comment init",
    r#"/// Doc comment
    init(){}"#
)]
#[test_case(
    "inner doc comment",
    r#"//! Doc comment1
    //! Doc comment2
    //! Doc comment3
    "#
)]
#[test_case("tuple", "(1, 1 + 1)")]
#[test_case("one-tuple", "(1,)")]
#[test_case("one-bracketed", "(1)")]
#[test_case("named tuple", "(length = 1, width = 1 + 1)")]
#[test_case("named one-tuple, trailing", "(length = 1,)")]
#[test_case("named one-tuple", "(length = 1)")]
#[test_case("partially named tuple", "(\"a\", length = 1)")]
#[test_case("empty tuple", "()")]
#[test_case("qualified name", "foo::bar")]
#[test_case("marker", "@input")]
#[test_case("unary", "!input")]
#[test_case("if", "if a > 1 { foo(); }")]
#[test_case("if-else", "if a > 1 { 3 } else { 4 }")]
#[test_case("else-if", "if a > 1 { 3 } else if a < -1 { 1 }")]
#[test_case("else-if-else", "if a > 1 { 3 } else if a < -1 { 1 } else { 0 }")]
#[test_case("sketch", "sketch Wheel(radius: Length) {std::geo2d::Circle(radius);}")]
#[test_case(
    "pub-part",
    "pub part Wheel(radius: Length, height = 1mm) {std::geo3d::Cylinder(radius, height);}"
)]
#[test_case(
    "sketch-with-init",
    "sketch Wheel(radius: Length) {
    init(diameter: Length) {
        radius = diameter / 2;
    }
    std::geo2d::Circle(radius);
}"
)]
#[test_case("mod", "mod foo { fn bar(){} }")]
#[test_case("mod pub", "pub mod foo { fn bar(){} }")]
#[test_case("mod extern", "mod foo;")]
#[test_case("use", "use foo;")]
#[test_case("use glob", "use foo::bar::*;")]
#[test_case("use as", "pub use foo::bar as foobar;")]
#[test_case("array list units", "a = [1, 2]mm;")]
#[test_case("array range units", "a = [1..3]mm;")]
#[test_case("const", "const FOO = 1;")]
#[test_case("pub const assignment", "pub const FOO = 1;")]
#[test_case("pub assignment", "pub FOO = 1;")]
#[test_case(
    "attribute assignment",
    r##"#[color = "#FF00FF"]
    a = 1;"##
)]
#[test_case(
    "attribute expression",
    r##"#[color = "#FF00FF"]
    1;"##
)]
#[test_case(
    "attribute trailing expression",
    r##"#[color = "#FF00FF"]
    1"##
)]
#[test_case(
    "attribute workbench",
    r##"#[color = "#FF00FF"]
    part Foo() {
        Cylinder(height = 10mm, radius = 5mm);
    }"##
)]
#[test_case(
    "attribute call",
    r##"#[svg(style = "fill:none")]
    a = 1;"##
)]
#[test_case(
    "attribute ident",
    r##"#[export]
    a = 1;"##
)]
#[test_case("attribute access", r##"foo#bar"##)]
#[test_case("array access", r##"foo[1]"##)]
#[test_case("tuple access", r##"foo.bar"##)]
#[test_case("method call", r##"foo.bar()"##)]
#[test_case("multiple element access", r##"foo[1]#bar.asd.call(1)"##)]
#[test_case("percent unit", "1%")]
#[test_case(
    "unclosed string",
    r#"a = "unclosed string;
    b = 1;"#
)]
#[test_case("units", r#"0" + 1% + 2mm / 3mm2 - 4mm³ * 1° + 1" - 2'"#; "units")]
#[test_case("array binary ops", r#"[1,2]" == [1,2]cm"#)]
#[test_case("unary binary ops", r#"-[1] != [1]"#)]
#[test_case("array comments", r#"[1 /* foo */,2 /* bar */]"#)]
#[test_case("un-typeable", r#"b = "sad"m3;"#)]
#[test_case("binary op precedence", "a^2 + b^3")]
#[test_case("inner attribute", r##"#![export]"##)]
#[test_case(
    "single trailing expr",
    r#"pub part A() {
        {
            B(C);
        }.align(D);
    }"#
)]
#[test_case("body expr method tail", "{}.foo()")]
#[test_case("body expr method", "{}.foo();")]
fn test_parser(name: &str, input: &str) {
    assert_debug_snapshot!(format!("parser_{name}"), parse(input));
}