///|
const PARSER_VERSION : String = "mbti-v1"
///|
fn collapse_whitespace(text : StringView) -> String {
let out = StringBuilder(size_hint=text.length())
let mut pending_space = false
for ch in text {
if ch.is_whitespace() {
pending_space = !out.is_empty()
} else {
if pending_space {
out.write_char(' ')
pending_space = false
}
out.write_char(ch)
}
}
out
.to_string()
.replace_all(old="@json.Json", new="Json")
.replace_all(old="@builtin.Json", new="Json")
.replace_all(old="@json.ToJson", new="ToJson")
.replace_all(old="@builtin.ToJson", new="ToJson")
.replace_all(old="@json.FromJson", new="FromJson")
.replace_all(old="@builtin.FromJson", new="FromJson")
}
///|
fn between_quotes(line : StringView, context : String) -> String raise {
let first = match line.find("\"") {
Some(index) => index
None =>
raise Failure("invalid .mbti: \{context} is missing an opening quote")
}
let rest = line[first + 1:]
let second = match rest.find("\"") {
Some(index) => index
None =>
raise Failure("invalid .mbti: \{context} is missing a closing quote")
}
rest[:second].to_owned()
}
///|
fn brace_delta(line : StringView) -> Int32 {
let mut depth = 0
for ch in line {
match ch {
'{' => depth = depth + 1
'}' => depth = depth - 1
_ => ()
}
}
depth
}
///|
fn is_ascii_space_code(code : UInt16) -> Bool {
code == 32 || (code >= 9 && code <= 13)
}
///|
fn is_declaration_start(line : StringView) -> Bool {
line.has_prefix("pub fn") ||
line.has_prefix("pub async fn") ||
line.has_prefix("pub let") ||
line.has_prefix("pub const") ||
line.has_prefix("pub using") ||
line.has_prefix("pub impl") ||
line.has_prefix("pub struct") ||
line.has_prefix("pub enum") ||
line.has_prefix("pub type") ||
line.has_prefix("pub suberror") ||
line.has_prefix("pub trait") ||
line.has_prefix("pub(all)") ||
line.has_prefix("pub(open)") ||
line.has_prefix("type ")
}
///|
fn declaration_kind(line : StringView) -> DeclarationKind {
if line.has_prefix("pub fn") || line.has_prefix("pub async fn") {
Function
} else if line.contains("struct") {
Struct
} else if line.contains("enum") {
Enum
} else if line.contains("suberror") || line.contains(" error ") {
Error
} else if line.contains("trait") {
Trait
} else if line.has_prefix("pub impl") || line.contains(" impl") {
Impl
} else if line.contains(" let ") ||
line.has_prefix("pub let") ||
line.has_prefix("pub const") {
Value
} else if line.has_prefix("pub using") {
Other
} else if line.contains(" type ") || line.has_prefix("type ") {
Type
} else {
Other
}
}
///|
fn next_token_after_marker(
line : StringView,
marker : StringView,
) -> String raise {
let marker_at = match line.find(marker) {
Some(index) => index
None => raise Failure("invalid .mbti declaration: missing `\{marker}`")
}
let mut index = marker_at + marker.length()
while index < line.length() && is_ascii_space_code(line[index]) {
index = index + 1
}
if index < line.length() && line[index] == '[' {
let mut bracket_depth = 0
while index < line.length() {
match line[index] {
'[' => bracket_depth = bracket_depth + 1
']' => {
bracket_depth = bracket_depth - 1
if bracket_depth == 0 {
index = index + 1
break
}
}
_ => ()
}
index = index + 1
}
while index < line.length() && is_ascii_space_code(line[index]) {
index = index + 1
}
}
let start = index
while index < line.length() &&
!is_ascii_space_code(line[index]) &&
line[index] != '(' &&
line[index] != '{' &&
line[index] != '[' {
index = index + 1
}
guard index > start else {
raise Failure("invalid .mbti declaration: cannot read name from `\{line}`")
}
line[start:index].to_owned()
}
///|
fn impl_name(line : StringView) -> String raise {
let marker_at = match line.find("impl") {
Some(index) => index
None => raise Failure("invalid .mbti declaration: missing `impl`")
}
let mut index = marker_at + 4
while index < line.length() && is_ascii_space_code(line[index]) {
index = index + 1
}
if index < line.length() && line[index] == '[' {
let mut depth = 0
while index < line.length() {
match line[index] {
'[' => depth = depth + 1
']' => {
depth = depth - 1
if depth == 0 {
index = index + 1
break
}
}
_ => ()
}
index = index + 1
}
while index < line.length() && is_ascii_space_code(line[index]) {
index = index + 1
}
}
line[index:].trim().to_owned()
}
///|
fn declaration_name(line : StringView, kind : DeclarationKind) -> String raise {
match kind {
Function => next_token_after_marker(line, "fn")
Struct => next_token_after_marker(line, "struct")
Enum => next_token_after_marker(line, "enum")
Error =>
if line.contains("suberror") {
next_token_after_marker(line, "suberror")
} else {
line.to_owned()
}
Trait => next_token_after_marker(line, "trait")
Type => next_token_after_marker(line, "type")
Value =>
if line.has_prefix("pub const") {
next_token_after_marker(line, "const")
} else {
next_token_after_marker(line, "let")
}
Impl => impl_name(line)
Other => line.to_owned()
}
}
///|
fn normalize_type_aliases(text : String) -> String {
let out = StringBuilder(size_hint=text.length())
let token = StringBuilder()
for ch in text {
if ch.is_ascii_alphabetic() || ch.is_ascii_digit() || ch == '_' {
token.write_char(ch)
} else {
if token.to_string() == "Int" {
out.write_string("Int32")
} else if token.to_string() == "UInt" {
out.write_string("UInt32")
} else {
out.write_string(token.to_string())
}
token.reset()
out.write_char(ch)
}
}
if token.to_string() == "Int" {
out.write_string("Int32")
} else if token.to_string() == "UInt" {
out.write_string("UInt32")
} else {
out.write_string(token.to_string())
}
out.to_string()
}
///|
fn sort_strings(values : Array[String]) -> Unit {
let mut index = 1
while index < values.length() {
let current = values[index]
let mut insert_at = index
while insert_at > 0 && values[insert_at - 1].lexical_compare(current) > 0 {
values[insert_at] = values[insert_at - 1]
insert_at = insert_at - 1
}
values[insert_at] = current
index = index + 1
}
}
///|
fn split_derive_traits(content : StringView) -> Array[String] {
let traits : Array[String] = []
let current = StringBuilder()
let mut in_arguments = false
for ch in content {
match ch {
'(' => in_arguments = true
')' => in_arguments = false
',' if !in_arguments => {
let trait_name = current.to_string().trim().to_owned()
if !trait_name.is_empty() {
traits.push(trait_name)
}
current.reset()
}
_ => if !in_arguments { current.write_char(ch) }
}
}
let trait_name = current.to_string().trim().to_owned()
if !trait_name.is_empty() {
traits.push(trait_name)
}
traits
}
///|
fn strip_derive(
canonical : String,
type_name : String,
) -> (String, Array[Declaration]) {
let marker_at = match canonical.find(" derive(") {
Some(index) => index
None => return (canonical, [])
}
let content_start = marker_at + " derive(".length()
let mut index = content_start
let mut depth = 1
while index < canonical.length() && depth > 0 {
match canonical[index] {
'(' => depth = depth + 1
')' => depth = depth - 1
_ => ()
}
index = index + 1
}
let derived = split_derive_traits(canonical[content_start:index - 1])
let stripped = canonical[:marker_at].trim().to_owned()
let implementation_names : Array[Declaration] = derived.map(fn(trait_name) {
{
kind: Impl,
name: "\{trait_name} for \{type_name}",
canonical: "pub impl \{trait_name} for \{type_name}",
line: 0,
}
})
(stripped, implementation_names)
}
///|
fn canonical_declaration(block : ArrayView[String]) -> String raise {
guard block.length() > 0 else {
raise Failure("invalid .mbti: empty declaration")
}
if block.length() == 1 {
return normalize_type_aliases(collapse_whitespace(block[0]))
}
let first = collapse_whitespace(block[0])
guard first.contains("{") &&
collapse_whitespace(block[block.length() - 1]) == "}" else {
return normalize_type_aliases(
block.map(fn(line) { collapse_whitespace(line) }).join(" "),
)
}
let body : Array[String] = []
for index in 1..<(block.length() - 1) {
let line = collapse_whitespace(block[index])
if !line.is_empty() && !line.has_prefix("//") {
body.push(line)
}
}
sort_strings(body)
normalize_type_aliases("\{first} \{body.join("; ")} }")
}
///|
fn make_declarations(
block : Array[String],
line_number : Int32,
) -> Array[Declaration] raise {
let first = block[0]
let kind = declaration_kind(first)
let name = declaration_name(first, kind)
let (canonical, derived_implementations) = strip_derive(
canonical_declaration(block),
name,
)
let declarations = [{ kind, name, canonical, line: line_number, }]
for implementation in derived_implementations {
declarations.push({ ..implementation, line: line_number, })
}
declarations
}
///|
fn append_declarations(
target : Array[Declaration],
declarations : Array[Declaration],
) -> Unit {
for declaration in declarations {
target.push(declaration)
}
}
///|
fn deduplicate_declarations(
declarations : Array[Declaration],
) -> Array[Declaration] {
let unique : Array[Declaration] = []
for declaration in declarations {
let mut duplicate = false
for existing in unique {
if existing.key() == declaration.key() {
duplicate = true
break
}
}
if !duplicate {
unique.push(declaration)
}
}
unique
}
///|
fn sort_declarations(declarations : Array[Declaration]) -> Unit {
let mut index = 1
while index < declarations.length() {
let current = declarations[index]
let mut insert_at = index
while insert_at > 0 &&
declarations[insert_at - 1].key().lexical_compare(current.key()) > 0 {
declarations[insert_at] = declarations[insert_at - 1]
insert_at = insert_at - 1
}
declarations[insert_at] = current
index = index + 1
}
}
///|
pub fn parse_api(
source : String,
source_name~ : String = "",
) -> ApiSnapshot raise {
let mut package_id = ""
let declarations : Array[Declaration] = []
let block : Array[String] = []
let mut block_line = 0
let mut depth = 0
let mut line_number = 0
for raw_line in source.split("\n") {
line_number = line_number + 1
let line = raw_line.trim()
if block.is_empty() {
if line.has_prefix("package ") {
if package_id.is_empty() {
package_id = between_quotes(line, "package declaration")
}
continue
}
if is_declaration_start(line) {
block.push(collapse_whitespace(line))
block_line = line_number
depth = brace_delta(line)
if depth == 0 {
append_declarations(
declarations,
make_declarations(block, block_line),
)
block.clear()
}
} else if line.has_prefix("pub") {
raise Failure(
"unsupported .mbti public syntax at \{source_name}:\{line_number}: \{line}",
)
}
} else {
block.push(collapse_whitespace(line))
depth = depth + brace_delta(line)
if depth == 0 {
append_declarations(declarations, make_declarations(block, block_line))
block.clear()
} else if depth < 0 {
raise Failure(
"invalid .mbti at \{source_name}:\{line_number}: unbalanced '}'",
)
}
}
}
if !block.is_empty() {
raise Failure(
"invalid .mbti at \{source_name}:\{block_line}: unterminated declaration block",
)
}
if package_id.is_empty() {
raise Failure("invalid .mbti: missing package declaration")
}
let unique = deduplicate_declarations(declarations)
sort_declarations(unique)
{ package_id, declarations: unique, source: source_name, }
}
///|
pub fn parser_version() -> String {
PARSER_VERSION
}