// Copyright © 2024-2025 The µcad authors <info@ucad.xyz>
// SPDX-License-Identifier: AGPL-3.0-or-later

// Grammar for the µcad language
// 
// Comments starting with //` generate tests for the grammar
// The strings inside the backticks (``) are the test input.
// 
// Example, the following comment generates two tests for the rule "array_type":
// 
// ```pest
// //`[Scalar]`: ok # Array with elements of type `Scalar`
// //`[]`: error # List must have a type, lists of elements with arbitrary type are not allowed
// array_type = { "[" ~ ws* ~ type ~ ws* ~ "]" }
// ```
// 
// See the crate microcad-pest-test for more info.

ws = _{ " " | "\n" | "\t" | "\r" }
// Whitespace

alpha_lower = _{ 'a'..'z' }
alpha_upper = _{ 'A'..'Z' }
alpha       = _{ alpha_lower | alpha_upper }
digit       = _{ '0'..'9' }

COMMENT = {
    single_line_comment
  | multi_line_comment
}

// `// ok`: ok
// `/ err` error
single_line_comment = ${ "//" ~ !"/" ~ (!"\n" ~ ANY)* }

// Documentation comments for µcad language
// `/// ok`: ok
// `// err` error
doc_comment = ${ "///" ~ (!"\n" ~ ANY)* }

// `/* ok */`: ok
multi_line_comment = { "/*" ~ (!"*/" ~ ANY)* ~ "*/" }

identifier_part = { alpha | "_" }
identifier_body = { (alpha | "_") ~ (alpha | digit | "_")* }
keywords        = { "mod" | "part" | "sketch" | "op" | "fn" | "if" | "else" | "use" | "return" }

// `a`: ok
// `a1`: ok
// `a_`: ok
// `_a`: ok
// `_`: ok
// `1`: error
// `.test`: error
identifier = { !(keywords ~ !identifier_part) ~ identifier_body }

// `1.0`: ok
// `.1`: ok
// `0.`: ok
// `1`: error: Missing decimal point
// `deadbeef`: error
number = @{ "-"? ~ (((int ~ "." ~ ASCII_DIGIT+) | (int ~ ".") | ("." ~ ASCII_DIGIT+)) ~ exp?) | exp }
int    = @{ "0" | ASCII_NONZERO_DIGIT ~ ASCII_DIGIT* }
exp    = @{ ("E" | "e") ~ ("+" | "-")? ~ ASCII_DIGIT+ }

// `90°`: ok, with `°` unit
// `5.0mm`: ok, with `mm` unit
// `90.0`: ok, no unit
// `mm`: error
number_literal = { number ~ unit? | integer_literal ~ unit }

// `-1`: ok
// `1`: ok
// `-1.0`: error # Is float
integer_literal = { "-" ~ int | int }

// `"a"`: ok
// `"a\n"`: ok
// `"a\""`: ok
// `"hello{10}world": ok
format_string        =  { "\"" ~ string_literal_inner? ~ (format_expression ~ string_literal_inner?)* ~ "\"" }
string_literal_inner =  { (!("\"" | "\\" | "{" | "}") ~ ANY)* ~ ((bracket_escape | escape) ~ string_literal_inner)? }
bracket_escape       =  { "{{" | "}}" }
escape               = @{ "\\" ~ ("\"" | "\\" | "/" | "b" | "f" | "n" | "r" | "t" | unicode) }
unicode              = @{ "u" ~ ASCII_HEX_DIGIT{4} }

// `{a}`: ok
// `{a + b}`: ok
format_expression = { "{" ~ ws* ~ (format_spec ~ ws* ~ ":")? ~ ws* ~ expression ~ ws* ~ "}" }

// `05.5`: ok # Five leading zeros with 5 precision
format_spec = {
    format_spec_width ~ format_spec_precision
  | format_spec_width
  | format_spec_precision
}

// `.5`: print with precision = 5
format_spec_precision = { "." ~ int }

// `05`: print with 5 leading zeros and precision = 0
format_spec_width = { "0" ~ int }

// `true`: ok
// `false`: ok
// `True`: error
// `False`: error
bool_literal = @{ "true" | "false" }

// `m`: ok
// `mm`: ok
// `µm`: ok
// `m²`: ok
// `mm³`: ok
// `kg/m²`: ok
// `kg/`: fail
// `kg/²`: fail
// `/kg`: fail
// `/1m`: fail
unit = @{ (alpha_lower+ ~ ((("/" ~ alpha_lower+)? ~ ("²" | "³" | "2" | "3")))?) | "°" | "%" | "\"" | "'" }

// `1`: ok # Integer
// `1mm`: ok # Number (Length)
// `1.0mm`: ok # Number (Length)
// `1.0`: ok # Scalar
// `true`: ok # Boolean
literal = {
    number_literal
  | integer_literal
  | bool_literal
}

// `a`: ok # Single identifier
// `a::b`: ok # Nested identifiers
// `a::b ::c`: error # No whitespace between identifiers
// `a::b::c::`: error # No trailing ::
qualified_name = { identifier ~ ("::" ~ identifier)* }

// `int`: ok
// `a::nested::typename`: ok
// `[Scalar]`: ok # List type
// `(a: Scalar, b: Scalar)`: ok # Named tuple type
type = {
    array_type
  | tuple_type
  | matrix_type
  | base_type
  | quantity_type
  | unit
}

base_type = { "Integer" | "String" | "Bool" | "Models" }

quantity_type = { "Scalar" | "Length" | "Area" | "Volume" | "Density" | "Angle" | "Weight" }

type_annotation = _{ ":" ~ ws* ~ type }

// `[scalar]`: ok list with elements of type `scalar`
// `[]`: error # List must have a type, lists of elements with arbitrary type are not allowed
array_type = { "[" ~ ws* ~ type ~ ws* ~ "]" }

// `(a: Length, b: scalar)`: ok
// `(a: Length, )`: ok # Trailing comma
tuple_type = {
    ("(" ~ ws* ~ tuple_type_element ~ (ws* ~ "," ~ ws* ~ tuple_type_element)* ~ ws* ~ ","? ~ ws* ~ ")")
  | ("Color" | "Vec2" | "Vec3" | "Size2")
}

// `a: Length`: ok
// `Length`: ok
tuple_type_element = { identifier ~ ws* ~ ":" ~ ws* ~ type | type }

// `Matrix4x3`: ok
// `Matrix3`: ok
matrix_type = { "Matrix" ~ int ~ ("x" ~ int)? }

range_start = { integer_literal | expression }
range_end   = { integer_literal | expression }

// `1..3`: ok
range_expression = { range_start ~ ws* ~ ".." ~ ws* ~ range_end }

// `[1, 2, 3]`: ok
// `[1, 2, 3,]`: error # Trailing comma
// `[]`: ok
array_expression = { "[" ~ ws* ~ (range_expression | list_expression?) ~ ws* ~ "]" ~ unit? }

// `[0]`: ok # Access first element
// `["test"]`: ok # Associative lists, access value of `\"test\"`
array_element_access = { "[" ~ ws* ~ expression ~ ws* ~ "]" }

// An expression for tuples
// `(1.0mm, 2.0mm)`: ok
// `(1.0mm,)`: ok # Trailing comma
// `(x = 1.0mm, y = 2.0mm)`: ok
tuple_expression = { "(" ~ ws* ~ argument_list ~ ws* ~ ")" }

// `.height`: ok
// `.=`: error # Neither an identifier nor an integer
tuple_element_access = { "." ~ identifier }

// `#layer`: ok
attribute_access = { "#" ~ identifier }

element_access = _{ method_call | array_element_access | tuple_element_access | attribute_access }

// `pub`: ok
visibility = { "pub" | "" }

// `use a;`: ok # Import a single module
// `use ;`: error # Must import at least one module
// `use foo::*;`: ok # Import all modules from foo
// `use foo as bar;`: ok # Import foo as bar
// `pub use foo;`: ok # Import and re-export foo
use_statement = { visibility? ~ ws* ~ "use" ~ ws+ ~ use_declaration ~ ws* ~ ";" }

use_declaration = { use_all | use_alias | qualified_name }

// `std::*`: ok # Import everything from std
use_all = { qualified_name ~ ws* ~ "::" ~ ws* ~ "*" }

// `std::math as my_math`: ok # Import math from std as my_math
use_alias = { qualified_name ~ ws+ ~ "as" ~ ws+ ~ identifier }

// `-5.0mm`: ok # Negate
// `(a+b)*(c-a)`: ok
// `a + b + c`: ok
// `math::PI`: ok # Singleton access with qualified name or module access
// `! a`: ok # Unary operator with whitespace
// `a-`: error # Unknown postfix operator `-`
// `a[2]`: ok # List access
// `a.b`: ok # Tuple access
// `(b = 3.0).b`: ok # Tuple access
// `a::b()`: ok # Method call
// `a::b().c`: ok # Nested method call
// `a::b.c`: ok # Nested tuple access
// `a and b`: ok # Binary operator
// `[3,5,5]`: ok # List expression
// `1 if 3 > 2 else 0`: ok # Ternary operator
// `a()`: ok # Simple call
// `translate() rotate()`: ok # Nested call
// `translate() rotate() { translate() rotate() {} }`: ok # Nested call
// `a | a::b()`: ok
// `a | b & c`: ok
// `a() b() | b & c | d`: ok
// `Circle(r = 5.0mm)`: ok
// `Circle(r = 5.0mm) {}`: ok
// `translate(x = 5.0mm) { rectangle(width = 5.0mm); }`: ok
// `translate(x = 5.0mm) { rotate(angle = 90°) { rectangle(width = 5.0mm); } }`: ok
// `translate(r = 5.0mm) Circle(r = 5.0mm)`: ok
// `{}`: ok # Empty body
// `{ rectangle(width = 5.0mm); }`: ok # Single statement body
// `translate(x = 5mm)`: ok
// `[hole_positions.bottom]`: ok # List access with tuple access
// `mountable_plate(hole_positions = hole_positions.edges - [hole_positions.bottom])`: ok # Tuple access
// `1000mm == 1m`: ok # Comparison
// `1000mm == 1m == 100cm`: ok # Multi comparison
expression     =  { unary_op* ~ ws* ~ primary ~ ws* ~ postfix* ~ (ws* ~ bin_op ~ ws* ~ unary_op* ~ ws* ~ primary ~ ws* ~ postfix*)* }
bin_op         = _{ add | subtract | multiply | divide | union | intersect | power_xor | greater_equal | less_equal | greater_than | less_than | equal | near | not_equal | and | or | xor | if_binary_op | else_binary_op }
add            =  { "+" }
subtract       =  { "-" }
multiply       =  { "*" }
divide         =  { "/" }
union          =  { "|" }
intersect      =  { "&" }
power_xor      =  { "^" }
greater_than   =  { ">" }
less_than      =  { "<" }
greater_equal  =  { ">=" }
less_equal     =  { "<=" }
equal          =  { "==" }
near           =  { "~" }
not_equal      =  { "!=" }
and            =  { "and" }
or             =  { "or" }
xor            =  { "xor" }
if_binary_op   =  { "if" }
else_binary_op =  { "else" }

unary_op    = _{ unary_minus | unary_plus | unary_not }
unary_minus =  { "-" }
unary_plus  =  { "+" }
unary_not   =  { "!" }
postfix     = _{ element_access }
primary     = _{ literal | marker | body | call | qualified_name | format_string | array_expression | "(" ~ ws* ~ expression ~ ws* ~ ")" | tuple_expression | if_statement }

// `a, b, c`: ok
// `1+2, 4*3, 3.2`: ok
// `,,`: error # No expression
list_expression = { expression ~ (ws* ~ "," ~ ws* ~ expression)* }

// A statement that is a single expression
expression_statement = { (attribute_list ~ ws+)? ~ expression ~ ws* ~ ";" }

// A statement that is a single expression, with optional semicolon
final_expression_statement = { (attribute_list ~ ws+)? ~ expression ~ (ws* ~ ";")? }

// `r = 5.0mm`: ok # Named argument
named_argument = { identifier ~ ws* ~ "=" ~ ws* ~ expression }

// `2.0mm`: ok # Positional argument
positional_argument = _{ expression }

argument = _{
    named_argument
  | positional_argument
}

// `a = 5.0mm, b = false`: ok
// `4.0mm, a = false`: ok
// `a = false, 4.0mm`: ok # Positional argument after named argument (Syntactically correct, semantically wrong)
// `a = false, 4.0mm,`: ok # Trailing comma
argument_list = { argument ~ (ws* ~ "," ~ ws* ~ argument)* ~ ws* ~ ","? }

call_op = _{ "(" ~ ws* ~ argument_list? ~ ws* ~ ")" }

// `.sum()`: ok
method_call = { "." ~ ws* ~ qualified_name ~ ws* ~ call_op ~ ws* }

// `f(a = 3.0mm)`: ok
// `f(a = 3.0mm, b = 2.0mm)`: ok
// `a::nested::f(a = 3.0mm, b = 2.0mm)`: ok
call = { qualified_name ~ ws* ~ call_op }

// `foo = bar`: ok # Assignment
// `export`: ok # Command with no commands
// `export = "test.svg"`: ok # Command as string
// `measure = width`: ok # Command as identifier without arguments
// `measure = width, height(offset = 30mm)`: ok # Item with multiple commands
attribute_item = _{ identifier ~ (ws* ~ "=" ~ ws* ~ attribute_command_list)? }

// `"test.svg"`: ok # Format string command
// `width(offset = 30mm)`: ok # Command with argument list.
// `(style = "fill:none"): ok # Anonymous argument list.
attribute_command = { expression | identifier ~ (ws* ~ call_op)? | call_op }

// `width, height`: ok # Multiple commands.
attribute_command_list = _{ attribute_command ~ (ws* ~ "," ~ ws* ~ attribute_command)* ~ ws* ~ ","? }

// `#[bar = width]`: ok # An attribute with command.
// `#[svg = (style = "test")]`: ok # Expression attribute
attribute = { "#[" ~ ws* ~ attribute_item ~ ws* ~ "]" }

// `#![size = A4]`: ok # Name-value attribute
inner_attribute = { "#![" ~ ws* ~ attribute_item ~ ws* ~ "]" }

inner_attribute_statement = _{ inner_attribute }

// `#[foo = 32] #[bar = 32]`: ok # Multiple attributes
attribute_list = { attribute ~ (ws+ ~ attribute)* }

// `const`: ok
// `prop`: ok
qualifier = { "const" | "prop" }

// `r: Length = 5.0mm`: ok
// `r = 5.0mm`: ok # Type specifier with type inference
// `r: Length`: error # Default value is required
// `r: Scalar = math::PI`: ok # Type specifier + Default value from constant
// `r`: error # neither type specifier or default value
// `const r = 5.0mm`: Make value of assignment a public constant
// `prop r = 5.0mm`: Make value of assignment a public property
assignment = { doc_block? ~ (visibility ~ ws+)? ~ (qualifier ~ ws+)? ~ identifier ~ ws* ~ type_annotation? ~ ws* ~ "=" ~ ws* ~ expression }

assignment_statement = { (attribute_list ~ ws+)? ~ assignment ~ ws* ~ ";" }

// `@input`: ok
marker = { "@" ~ identifier }

// Rule for a single parameter of a definition
// `r: Length = 5.0mm`: ok
// `r = 5.0mm`: ok # Type specifier with type inference
// `r: Length`: ok # Default value is NOT required
// `r: Scalar = math::PI`: ok # Type specifier + Default value from constant
// `r`: error # neither type specifier or default value
parameter = { identifier ~ ws* ~ type_annotation? ~ ws* ~ "=" ~ ws* ~ expression | identifier ~ ws* ~ type_annotation }

// `(r = 5.0mm, b = 4.0mm)`: ok
// `(r = 5.0mm, b = 4.0mm, )`: ok
parameter_list = { "(" ~ ws* ~ (parameter ~ (ws* ~ "," ~ ws* ~ parameter)* ~ ws*)? ~ ","? ~ ws* ~ ")" }

// `(a: scalar) -> scalar`: ok # Function signature with arguments
// `() -> scalar`: ok # Function signature with no arguments
// `(a: scalar, b: scalar) -> scalar`: ok # Function signature with multiple arguments
// `(a: scalar = 2.0, b: scalar = 3.0) -> scalar`: ok # Function signature with default values
// `(a: scalar = 2.0, b: scalar = 3.0)`: ok # Function signature without return type
function_signature = { parameter_list ~ (ws* ~ "->" ~ ws* ~ type)? }

// `fn foo() -> Scalar { return math::pi; }`: ok
// `fn foo() -> Scalar {}`: ok # Empty function
// `fn foo() { return 5.0mm; }`: ok # Function return type is inferred
// `fn foo(a: Scalar) -> Scalar { return a; }`: ok # Function with arguments
// `fn foo(a: Scalar, b: Scalar) -> Scalar { return a + b; }`: ok # Function with multiple arguments
// `fn foo(a: Scalar, b: Scalar) -> Scalar { return a + b; }`: ok # Function with multiple arguments
// `fn test(a: Scalar, b: Scalar) -> Scalar { c = 1.0; return a + b + c; }`: ok # Function with multiple arguments and local variable
// `fn weight(density: density = 20g/1mm^3) -> Weight {}` ok # Function with default value
// `pub fn foo() {}` ok # Public function
function_definition = { doc_block? ~ (visibility ~ ws+)? ~ "fn" ~ ws+ ~ identifier ~ ws* ~ function_signature ~ ws* ~ body }

// `return 5.0mm;`: ok
return_statement = { "return" ~ ws* ~ expression ~ ws* ~ ";" }

// `if a > 5.0mm { return 5.0mm; }`: ok
// `if a > 5.0mm { return 5.0mm; } else { return 4.0mm; }`: ok
if_statement = { "if" ~ ws* ~ expression ~ ws* ~ body ~ (ws* ~ "else" ~ ws* ~ (body | if_statement))? }

// Documentation block.
doc_block = { (doc_comment ~ ws+)+ }

workbench_kind = { "sketch" | "part" | "op" }

// `part Foo() { }`: ok # Part with no properties
// `part Foo(a: scalar) { }`: ok # Part with properties
// `part Foo(a: scalar, b: scalar) { }`: ok # Part with multiple properties
// `part Foo() { part Bar() { } }`: ok # Nested part definition
// `part Foo() { a: scalar = 5.0; fn bar() -> scalar { return 1; } }`: ok # Parameter and nested function
// `part List(count: int, delegate = {}) { translate(x = [0..count]*5mm) delegate; }`: ok # Part with part as properties
// `part Foo() { init(size: Length) { rectangle(size); } }`: ok # Part with init function
// `part Foo() { fn bar(a: Scalar) -> Scalar { a } }`: ok # Part with function
// `part Donut(radius) { }`: error # Part property must have type annotations
// `sketch Foo() { }`: ok # Sketch with no properties
// `op foo() { }`: ok # Sketch with no properties
// `pub sketch Foo() { }`: ok # public sketch
workbench_definition = { doc_block? ~ (attribute_list ~ ws+)? ~ (visibility ~ ws+)? ~ workbench_kind ~ ws+ ~ identifier ~ ws* ~ parameter_list ~ ws* ~ body }

// `{ translate() rotate(); }`: ok
// `{}`: ok # Empty body
body = { "{" ~ ws* ~ statement_list ~ ws* ~ "}" }

// `init(d: Length) { r = d / 2; }`: ok
// `init(size: Length) { size = size; }`: ok
// `init(size: Length) { rectangle(size); }: ok
init_definition = { doc_block? ~ "init" ~ ws* ~ parameter_list ~ ws* ~ body }

// `mod math { }`: ok # module
// `mod math { fn pi() -> Scalar { return 3.14159; } }`: ok # module with function
module_definition = { doc_block? ~ (visibility ~ ws+)? ~ "mod" ~ ws+ ~ identifier ~ ws* ~ (body | ";") }

statement = {
    workbench_definition
  | module_definition
  | function_definition
  | init_definition
  | use_statement
  | return_statement
  | if_statement
  | inner_attribute_statement
  | assignment_statement
  | expression_statement
  | doc_block
}

statement_list = { (statement ~ ws*)* ~ final_expression_statement? }

// `Rect(width = 5.0mm).translate(x = 5.0mm); Rect(width = 5.0).translate(x = 5.0mm).rotate(angle = 90°);`: ok
// `part Foo() { init(size: Length) {} Rectangle(size); }`: ok # Part with init function
source_file = { SOI ~ doc_block? ~ ws* ~ statement_list ~ ws* ~ EOI }
