// clojure.pest
// Copyright 2024 Matti Hänninen
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
COMMENT = @{ comment_prefix ~ comment_char* }
comment_prefix = { ";" | "#!" }
comment_char = { ! NEWLINE ~ ANY}
WHITESPACE = @{
( '\u{09}'..'\u{0D}' // whitespace control chars
| " "
| ","
)+
}
//
// Top-level, forms, and expressions
//
top_level = { SOI ~ form* ~ discarded_form? ~ EOI }
form = {
expr
| quote_unquote_form
| preform+ ~ form
}
quote_unquote_form = { quote_unquote_prefix ~ form }
quote_unquote_prefix = { "'" | "#'" | "`" | "~@" | "~" }
preform = { meta_form | discarded_form }
// XXX(soija) Given that the "form" reulf now takes optional preforms as prefix
// itself, could we drop the preform form the "discarded_form" rule?
// FIXME: Test and drop.
discarded_form = { discard_prefix ~ preform* ~ form }
discard_prefix = { "#_" }
meta_form = { meta_prefix ~ form }
meta_prefix = { "^" | "#^" }
expr = {
nil
| boolean
| number
| char
| string
| regex
| symbolic_value
| symbol
| keyword
| list
| vector
| set
| anonymous_fn
| map
| reader_conditional
| tagged_literal
}
//
// Nils
//
nil = ${ "nil" ~ ! symbol_char }
//
// Booleans
//
boolean = ${ ( "true" | "false" ) ~ ! symbol_char }
//
// Numeric literals
//
number = ${ sign? ~ unsigned_number ~ ! ASCII_ALPHANUMERIC }
sign = { "-" | "+" }
unsigned_number = _{
unsigned_bigfloat
| unsigned_float
| unsigned_ratio
| unsigned_radix_int
| unsigned_int
}
unsigned_bigfloat = @{ whole ~ fractional? ~ base10_exp? ~ "M" }
unsigned_float = @{ whole ~ ( fractional ~ base10_exp? | base10_exp ) }
whole = @{ ASCII_DIGIT+ }
fractional = @{ "." ~ ASCII_DIGIT* }
base10_exp = ${ ^"e" ~ sign? ~ exponent }
exponent = @{ ASCII_DIGIT+ }
unsigned_ratio = ${ numerator ~ "/" ~ denominator }
numerator = @{ ASCII_DIGIT+ }
denominator = @{ ASCII_DIGIT+ }
unsigned_radix_int = ${ radix ~ ^"r" ~ radix_digits }
radix = @{ unsigned_dec }
radix_digits = @{ ASCII_ALPHANUMERIC+ }
unsigned_int = ${
( unsigned_oct
| unsigned_hex
| unsigned_dec
)
~ bigint_suffix? }
bigint_suffix = { "N" }
unsigned_hex = _{ hex_prefix ~ hex_digits }
hex_prefix = _{ ^"0x" }
hex_digits = @{ ASCII_HEX_DIGIT + }
unsigned_oct = _{ oct_prefix ~ oct_digits }
oct_prefix = _{ "0" }
oct_digits = @{ ASCII_OCT_DIGIT + }
unsigned_dec = @{ ASCII_NONZERO_DIGIT ~ ASCII_DIGIT* | "0" }
//
// Character literals
//
char = ${
"\\"
~ ( char_name
| "o" ~ char_octal
| "u" ~ char_code_point
| char_simple
)
~ ! continuation_char
}
char_name = {
"newline"
| "space"
| "tab"
| "formfeed"
| "backspace"
}
char_octal = @{ ASCII_OCT_DIGIT{1,3} }
char_code_point = @{ ASCII_HEX_DIGIT{4} }
char_simple = @{ ! ( '\u{00}'..'\u{1F}' | " " ) ~ ANY }
// XXX(soija) TODO: consolidate with `symbol_char` and `symbol_first_char`
continuation_char = @{
! ( '\u{00}'..'\u{1F}'
| " " | ","
| "^" | "`" | "~"
| "(" | ")" | "[" | "]" | "{" | "}"
| "\"" | "\\"
)
~ ANY
}
//
// String literals
//
string = ${
"\""
~ ( unescaped
| esc_char
| esc_octet
| esc_code_point
)*
~ "\""
}
unescaped = @{ unescaped_char+ }
unescaped_char = {
! ( '\u{00}'..'\u{08}'
| '\u{0E}'..'\u{1F}'
| "\u{7F}"
| "\\"
| "\""
)
~ ANY
}
esc_char = @{ "\\" ~ ( "b" | "t" | "n" | "f" | "r" | "\"" | "\\" ) }
esc_octet = @{
"\\" ~ ( '0'..'3' ~ '0'..'7'{2}
| '0'..'7' {1,2} ~ & ( "\"" | "\\" )
)
}
esc_code_point = @{ "\\u" ~ ASCII_HEX_DIGIT{4} }
//
// Regex pattern literals
//
regex = ${ "#\"" ~ regex_content ~ "\"" }
regex_content = @{
( "\\" ~ ! control_char ~ ANY
| ! ( control_char | "\"" ) ~ ANY
)*
}
control_char = {
'\u{00}'..'\u{08}'
| '\u{0E}'..'\u{1F}'
| "\u{7F}"
}
//
// Symbolic values
//
symbolic_value = { symbolic_value_prefix ~ unqualified_symbol }
symbolic_value_prefix = { "##" }
//
// Symbols
//
symbol = ${
! "##"
~ ( namespace ~ "/" ~ qualified_symbol
| unqualified_symbol
)
}
namespace = @{
symbol_first_char
~ symbol_char*
~ ( ":" ~ symbol_char+ )*
}
unqualified_symbol = @{
namespace
| "/" ~ ! ( symbol_char | "/" | ":" )
}
qualified_symbol = @{
( "/"* ~ ":"? ~ symbol_char+ )+
| "/" ~ ! ":"
}
symbol_char = @{ symbol_first_char | "'" | "#" | ASCII_DIGIT }
symbol_first_char = @{
! ( '\u{00}'..'\u{1F}'
| " " | ","
| ASCII_DIGIT
| ":" | "^" | "'" | "`" | "~" | "/" | "#"
| "(" | ")" | "[" | "]" | "{" | "}"
| "\"" | "\\"
)
~ ANY
}
//
// Keywords
//
keyword = ${
keyword_prefix
~ ( namespace ~ "/" ~ qualified_symbol
| unqualified_keyword
)
}
keyword_prefix = @{ "::" | ":" }
unqualified_keyword = @{
symbol_char+ ~ ( ":" ~ symbol_char+ )*
| "/" ~ ! ( symbol_char | "/" | ":" )
}
//
// Lists, vectors, and the like
//
list = { "(" ~ seq_body ~ ")" }
vector = { "[" ~ seq_body ~ "]" }
anonymous_fn = { "#(" ~ seq_body ~ ")"}
set = { "#{" ~ seq_body ~ "}" }
seq_body = _{ form* ~ discarded_form? }
//
// Maps
//
map = { map_qualifier? ~ unqualified_map }
map_qualifier = ${ map_qualifier_prefix ~ namespace }
map_qualifier_prefix = { "#::" | "#:" }
unqualified_map = { "{" ~ map_body ~ "}" }
reader_conditional = { reader_conditional_prefix ~ reader_conditional_body }
reader_conditional_prefix = { "#?@" | "#?" }
reader_conditional_body = { "(" ~ map_body ~ ")" }
map_body = _{ map_entry* ~ discarded_form? }
map_entry = _{ form ~ form }
//
// Tagged literals
//
tagged_literal = {
! "#_"
~ "#"
~ tagged_literal_tag
~ form
}
tagged_literal_tag = { preform* ~ symbol }