use crate::parser::diagnostics::{DiagnosticKind, ParseDiagnostic};
use crate::syntax::{SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken};
use rowan::NodeOrToken;
use std::cell::RefCell;
use SyntaxKind::*;
thread_local! {
static PROJ_DIAGS: RefCell<Vec<(DiagnosticKind, usize, usize)>> =
const { RefCell::new(Vec::new()) };
}
pub fn to_juliasyntax_sexpr(tree: &SyntaxNode, diags: &[ParseDiagnostic]) -> String {
PROJ_DIAGS.with(|d| {
*d.borrow_mut() = diags.iter().map(|x| (x.kind, x.start, x.end)).collect();
});
project(tree)
}
fn diag_count_at(pos: usize, kind: DiagnosticKind) -> usize {
PROJ_DIAGS.with(|d| {
d.borrow()
.iter()
.filter(|(k, s, e)| *k == kind && *s == pos && *e == pos)
.count()
})
}
fn diag_at(pos: usize, kind: DiagnosticKind) -> bool {
diag_count_at(pos, kind) > 0
}
fn diag_count_from(pos: usize, kind: DiagnosticKind) -> usize {
PROJ_DIAGS.with(|d| {
d.borrow()
.iter()
.filter(|(k, s, _)| *k == kind && *s == pos)
.count()
})
}
fn keyword_start(node: &SyntaxNode) -> usize {
if node.kind() == DO_EXPR
&& let Some(do_kw) = node
.children_with_tokens()
.filter_map(|el| el.into_token())
.find(|t| t.kind() == DO_KW)
{
return usize::from(do_kw.text_range().start());
}
node.descendants_with_tokens()
.filter_map(|el| el.into_token())
.find(|t| !is_trivia(t.kind()))
.map(|t| usize::from(t.text_range().start()))
.unwrap_or_else(|| usize::from(node.text_range().start()))
}
fn is_recovery_error(node: &SyntaxNode) -> bool {
let r = node.text_range();
if r.start() == r.end() {
return false;
}
let (s, e) = (usize::from(r.start()), usize::from(r.end()));
PROJ_DIAGS.with(|d| {
d.borrow().iter().any(|(k, ds, de)| {
matches!(
k,
DiagnosticKind::StrayCloser
| DiagnosticKind::TrailingJunk
| DiagnosticKind::ImportRecoveryColon
) && *ds >= s
&& *de <= e
})
})
}
fn stray_keyword_text(node: &SyntaxNode) -> Option<String> {
let r = node.text_range();
let (s, e) = (usize::from(r.start()), usize::from(r.end()));
let has_diag = PROJ_DIAGS.with(|d| {
d.borrow()
.iter()
.any(|(k, ds, de)| matches!(k, DiagnosticKind::StrayKeyword) && *ds >= s && *de <= e)
});
if !has_diag {
return None;
}
node.children_with_tokens().find_map(|el| match el {
NodeOrToken::Token(t) if is_keyword(t.kind()) => Some(t.text().to_string()),
_ => None,
})
}
pub fn normalize_sexpr(s: &str) -> String {
let mut tokens: Vec<String> = Vec::new();
let bytes = s.as_bytes();
let mut i = 0usize;
while i < bytes.len() {
let c = bytes[i];
match c {
b' ' | b'\t' | b'\n' | b'\r' => i += 1,
b'(' | b')' => {
tokens.push((c as char).to_string());
i += 1;
}
b'"' => {
let start = i;
i += 1;
while i < bytes.len() {
match bytes[i] {
b'\\' if i + 1 < bytes.len() => i += 2,
b'"' => {
i += 1;
break;
}
_ => i += 1,
}
}
tokens.push(s[start..i].to_string());
}
_ => {
let start = i;
while i < bytes.len() {
if matches!(bytes[i], b' ' | b'\t' | b'\n' | b'\r' | b'(' | b')' | b'"') {
break;
}
i += 1;
}
if i > start {
tokens.push(s[start..i].to_string());
}
}
}
}
tokens.join(" ")
}
fn project(node: &SyntaxNode) -> String {
match node.kind() {
ROOT => sexp("toplevel", stmt_strings(node)),
TOPLEVEL_SEMICOLON => sexp("toplevel-;", stmt_strings(node)),
DOC => sexp("doc", stmt_strings(node)),
BLOCK => sexp("block", stmt_strings(node)),
BEGIN_EXPR => project_block_child_folding_error(node),
NAME => name_text(node),
LITERAL => project_literal(node),
STRING_LITERAL => project_string(node),
NONSTANDARD_IDENTIFIER => project_var(node),
CMD_LITERAL => project_cmd(node),
INTERPOLATION => {
let inner = project_interpolation(node);
if inner.is_empty() && !node.children_with_tokens().any(|el| el.kind() == LPAREN) {
"$".to_string()
} else {
format!("($ {inner})")
}
}
PAREN_EXPR | CONDITION => match first_node(node) {
Some(inner) => project(&inner),
None => significant(node)
.iter()
.find_map(|el| match el {
NodeOrToken::Token(t) if is_operator(t.kind()) => Some(t.text().to_string()),
_ => None,
})
.unwrap_or_else(|| "(block)".to_string()),
},
BINARY_EXPR => project_binary(node),
RANGE_EXPR => project_range(node),
COMPARISON_EXPR => project_comparison(node),
ASSIGNMENT_EXPR => project_assignment(node),
UNARY_EXPR => project_unary(node),
POSTFIX_EXPR => project_postfix(node),
SPLAT_EXPR => sexp("...", vec![project_first(node)]),
TYPE_ANNOTATION => project_type_annotation(node),
WHERE_EXPR => project_where(node),
ARROW_EXPR => sexp("->", project_each(child_nodes(node))),
JUXTAPOSE_EXPR => project_juxtapose(node),
TERNARY_EXPR => project_ternary(node),
CALL_EXPR => project_call("call", node),
INDEX_EXPR => project_call("ref", node),
CURLY_EXPR => project_call("curly", node),
DOT_CALL_EXPR => project_dot_call(node),
BRACES => sexp("braces", project_args(node)),
TUPLE_EXPR => sexp("tuple-p", project_args(node)),
PAREN_BLOCK => sexp("block-p", project_block_args(node)),
BARE_TUPLE_EXPR => sexp("tuple", project_each(child_nodes(node))),
VECT_EXPR => sexp("vect", project_args(node)),
MATRIX_EXPR => project_matrix(node),
TYPED_MATRIX_EXPR => project_typed_matrix(node),
BRACESCAT_EXPR => project_bracescat(node),
COMPREHENSION => sexp("comprehension", vec![project_generator(node)]),
BRACES_COMPREHENSION => sexp("braces", vec![project_generator(node)]),
TYPED_COMPREHENSION => project_typed_comprehension(node),
GENERATOR => project_generator(node),
IF_EXPR => project_if(node),
WHILE_EXPR => sexp("while", project_each(child_nodes(node))),
FOR_EXPR => {
let mut parts = vec![project_for_binding(node), project_block_child(node)];
push_trailing_errors(node, &mut parts);
sexp("for", parts)
}
FUNCTION_DEF => project_function_like("function", node),
MACRO_DEF => project_function_like("macro", node),
LET_EXPR => project_let(node),
QUOTE_EXPR => sexp("quote", vec![project_block_child_folding_error(node)]),
QUOTE_SYM => project_quote_sym(node),
TRY_EXPR => project_try(node),
STRUCT_DEF => project_struct(node),
ABSTRACT_DEF => sexp("abstract", vec![project_signature(node)]),
PRIMITIVE_DEF => project_primitive(node),
MODULE_DEF => project_module(node),
DO_EXPR => project_do(node),
RETURN_EXPR => project_keyword_stmt("return", node),
BREAK_EXPR => "(break)".to_string(),
CONTINUE_EXPR => "(continue)".to_string(),
CONST_STMT => {
let decl = project_decl("const", node);
if diag_at(
usize::from(node.text_range().start()),
DiagnosticKind::ConstNotAssignment,
) {
format!("(error {decl})")
} else {
decl
}
}
GLOBAL_STMT => project_decl("global", node),
LOCAL_STMT => project_decl("local", node),
IMPORT_STMT => project_import("import", node),
USING_STMT => project_import("using", node),
EXPORT_STMT => project_export(node),
PUBLIC_STMT => project_public(node),
IMPORT_PATH => project_import_path(node),
IMPORT_ALIAS => project_import_alias(node),
MACRO_CALL => project_macrocall(node),
END_MARKER => "end".to_string(),
BEGIN_MARKER => "begin".to_string(),
OPERATOR_ATOM => project_operator_atom(node),
ERROR => {
if let Some(kw) = stray_keyword_text(node) {
format!("(error {kw})")
} else {
project_error(
if is_recovery_error(node) {
"error-t"
} else {
"error"
},
node,
)
}
}
other => format!("(unsupported {other:?})"),
}
}
enum InfixHead {
CallI(&'static str),
Special(&'static str),
Dot,
DotCallI(&'static str),
}
fn infix_head(kind: SyntaxKind) -> InfixHead {
use InfixHead::*;
match kind {
PLUS => CallI("+"),
MINUS => CallI("-"),
STAR => CallI("*"),
STAR_STAR => CallI("(Error**)"),
MINUS_MINUS => CallI("(ErrorInvalidOperator)"),
SLASH => CallI("/"),
BACKSLASH => CallI("\\"),
SLASH_SLASH => CallI("//"),
CARET => CallI("^"),
PERCENT => CallI("%"),
COLON => CallI(":"),
DOT_DOT => CallI(".."),
FAT_ARROW => CallI("=>"),
PIPE_GT => CallI("|>"),
PIPE_LT => CallI("<|"),
LONG_ARROW => Special("-->"),
LEFT_RIGHT_ARROW => CallI("<-->"),
LEFT_LONG_ARROW => CallI("<--"),
SHL => CallI("<<"),
SHR => CallI(">>"),
USHR => CallI(">>>"),
AMP => CallI("&"),
PIPE => CallI("|"),
EQ_EQ => CallI("=="),
NOT_EQ => CallI("!="),
EQ_EQ_EQ => CallI("==="),
NOT_EQ_EQ => CallI("!=="),
LT => CallI("<"),
LE => CallI("<="),
GT => CallI(">"),
GE => CallI(">="),
TILDE => CallI("~"),
AND_AND => Special("&&"),
OR_OR => Special("||"),
DOT_AND_AND => Special(".&&"),
DOT_OR_OR => Special(".||"),
SUBTYPE => Special("<:"),
SUPERTYPE => Special(">:"),
EQ => Special("="),
DOT_EQ => Special(".="),
DOT => Dot,
DOT_PLUS => DotCallI("+"),
DOT_MINUS => DotCallI("-"),
DOT_STAR => DotCallI("*"),
DOT_STAR_STAR => DotCallI("(Error**)"),
DOT_MINUS_MINUS => DotCallI("(ErrorInvalidOperator)"),
DOT_SLASH => DotCallI("/"),
DOT_BACKSLASH => DotCallI("\\"),
DOT_SLASH_SLASH => DotCallI("//"),
DOT_CARET => DotCallI("^"),
DOT_PERCENT => DotCallI("%"),
DOT_TILDE => DotCallI("~"),
DOT_EQ_EQ => DotCallI("=="),
DOT_NOT_EQ => DotCallI("!="),
DOT_EQ_EQ_EQ => DotCallI("==="),
DOT_NOT_EQ_EQ => DotCallI("!=="),
DOT_LT => DotCallI("<"),
DOT_LE => DotCallI("<="),
DOT_GT => DotCallI(">"),
DOT_GE => DotCallI(">="),
DOT_SUBTYPE => DotCallI("<:"),
DOT_SUPERTYPE => DotCallI(">:"),
DOT_FAT_ARROW => DotCallI("=>"),
DOT_LONG_ARROW => DotCallI("-->"),
DOT_LEFT_LONG_ARROW => DotCallI("<--"),
DOT_LEFT_RIGHT_ARROW => DotCallI("<-->"),
DOT_PIPE_GT => DotCallI("|>"),
DOT_AMP => DotCallI("&"),
DOT_PIPE => DotCallI("|"),
_ => CallI("?"),
}
}
fn project_operator_atom(node: &SyntaxNode) -> String {
significant(node)
.iter()
.find_map(|el| match el {
NodeOrToken::Token(t) if matches!(infix_head(t.kind()), InfixHead::DotCallI(_)) => {
Some(if op_has_suffix(t.text()) {
format!("(. {})", &t.text()[1..])
} else {
operator_func_repr(t.kind())
})
}
NodeOrToken::Token(t)
if t.text().starts_with('.')
&& t.text().len() > 1
&& t.text().as_bytes()[1] != b'.' =>
{
Some(format!("(. {})", &t.text()[1..]))
}
NodeOrToken::Token(t) => Some(t.text().to_string()),
_ => None,
})
.unwrap_or_else(|| "(operator)".to_string())
}
fn operator_func_repr(kind: SyntaxKind) -> String {
if kind == BANG {
return "!".to_string();
}
match infix_head(kind) {
InfixHead::CallI(s) | InfixHead::Special(s) => s.to_string(),
InfixHead::DotCallI(s) => format!("(. {s})"),
InfixHead::Dot => ".".to_string(),
}
}
fn op_has_suffix(text: &str) -> bool {
text.chars()
.next_back()
.is_some_and(super::lexer::is_op_suffix_char)
}
fn is_operator(kind: SyntaxKind) -> bool {
kind.is_operator()
}
fn project_ternary(node: &SyntaxNode) -> String {
let nodes = child_nodes(node);
let q_end = node
.children_with_tokens()
.filter_map(|el| el.into_token())
.find(|t| t.kind() == QUESTION)
.map(|t| usize::from(t.text_range().end()));
if let Some(qe) = q_end {
let markers = diag_count_at(qe, DiagnosticKind::IncompleteTernaryIf);
if markers > 0 {
let mut parts: Vec<String> = nodes.iter().map(project).collect();
for _ in 0..markers {
parts.push("(error-t)".to_string());
}
return sexp("if", parts);
}
}
let mut parts = Vec::new();
for (i, n) in nodes.iter().enumerate() {
parts.push(project(n));
if i == 0 {
if let Some(qe) = q_end {
for _ in 0..diag_count_at(qe, DiagnosticKind::TernaryQWhitespace) {
parts.push("(error-t)".to_string());
}
}
} else if i == 1 {
let then_end = usize::from(n.text_range().end());
for _ in 0..diag_count_at(then_end, DiagnosticKind::TernaryColonWhitespace) {
parts.push("(error-t)".to_string());
}
}
}
sexp("?", parts)
}
fn project_juxtapose(node: &SyntaxNode) -> String {
let nodes = child_nodes(node);
let mut parts = Vec::new();
for (i, n) in nodes.iter().enumerate() {
parts.push(project(n));
if i == 0
&& diag_at(
usize::from(n.text_range().end()),
DiagnosticKind::StringJuxtapose,
)
{
parts.push("(error-t)".to_string());
}
}
sexp("juxtapose", parts)
}
fn project_binary(node: &SyntaxNode) -> String {
if let Some(word) = node
.children_with_tokens()
.filter_map(|el| el.into_token())
.find(|t| t.kind() == IDENT && matches!(t.text(), "in" | "isa"))
{
let operands = child_nodes(node);
if operands.len() == 2 {
return format!(
"(call-i {} {} {})",
project(&operands[0]),
word.text(),
project(&operands[1])
);
}
}
let op = match operator_token(node) {
Some(t) => t,
None => return format!("(unsupported {:?})", node.kind()),
};
if op.kind() == COLON
&& diag_at(
usize::from(op.text_range().start()),
DiagnosticKind::InvalidGluedOperator,
)
{
let head: Vec<String> = node
.children_with_tokens()
.filter_map(|el| el.into_token())
.filter(|t| is_operator(t.kind()))
.map(|t| t.text().to_string())
.collect();
let operands = child_nodes(node);
let lhs = project(&operands[0]);
let rhs = operands
.get(1)
.map(project)
.unwrap_or_else(|| "(error)".to_string());
return format!("(call-i {lhs} (error {}) {rhs})", head.join(" "));
}
let dot_error = if diag_at(
usize::from(op.text_range().end()),
DiagnosticKind::DotWhitespace,
) {
"(error-t) "
} else {
""
};
let operands = child_nodes(node);
if operands.len() == 1 && operator_missing_rhs(&op) {
let lhs = project(&operands[0]);
return infix_call_string(&op, &lhs, "(error)")
.unwrap_or_else(|| format!("(. {lhs} {dot_error}(error))"));
}
let last_op_missing = node
.children_with_tokens()
.filter_map(|el| el.into_token())
.filter(|t| is_operator(t.kind()))
.last()
.is_some_and(|t| operator_missing_rhs(&t));
if operands.len() >= 3 || (operands.len() == 2 && last_op_missing) {
return project_flat_arith(&op, &operands, last_op_missing);
}
if operands.len() != 2 {
return project_flat(significant(node));
}
let lhs = project(&operands[0]);
let rhs = &operands[1];
let rhs_str = project(rhs);
if let Some(s) = infix_call_string(&op, &lhs, &rhs_str) {
return s;
}
match infix_head(op.kind()) {
InfixHead::Dot if rhs.kind() == INTERPOLATION => {
format!("(. {lhs} {dot_error}(inert {rhs_str}))")
}
InfixHead::Dot if rhs.kind() == QUOTE_SYM => {
format!("(. {lhs} {dot_error}{rhs_str})")
}
InfixHead::Dot => format!("(. {lhs} {dot_error}(quote {}))", name_text(rhs)),
_ => unreachable!("non-dot infix head handled by infix_call_string"),
}
}
fn project_flat_arith(op: &SyntaxToken, operands: &[SyntaxNode], last_op_missing: bool) -> String {
let (head, op_text) = match infix_head(op.kind()) {
InfixHead::CallI(text) => ("call-i", text.to_string()),
InfixHead::DotCallI(text) => ("dotcall-i", text.to_string()),
InfixHead::Special(text) => ("call-i", text.to_string()),
InfixHead::Dot => ("call-i", op.text().to_string()),
};
let mut parts = Vec::with_capacity(operands.len() + 2);
parts.push(project(&operands[0]));
parts.push(op_text);
parts.extend(operands[1..].iter().map(project));
if last_op_missing {
parts.push("(error)".to_string());
}
sexp(head, parts)
}
fn infix_call_string(op: &SyntaxToken, lhs: &str, rhs: &str) -> Option<String> {
match op.kind() {
UNICODE_OP if op.text().starts_with('.') => {
return Some(format!("(dotcall-i {lhs} {} {rhs})", &op.text()[1..]));
}
UNICODE_OP => return Some(format!("(call-i {lhs} {} {rhs})", op.text())),
UNICODE_ASSIGN_OP => return Some(format!("({} {lhs} {rhs})", op.text())),
_ => {}
}
if op_has_suffix(op.text()) {
let text = op.text();
return Some(match infix_head(op.kind()) {
InfixHead::DotCallI(_) => {
format!("(dotcall-i {lhs} {} {rhs})", text.trim_start_matches('.'))
}
_ => format!("(call-i {lhs} {text} {rhs})"),
});
}
match infix_head(op.kind()) {
InfixHead::CallI(text) => Some(format!("(call-i {lhs} {text} {rhs})")),
InfixHead::Special(text) => Some(format!("({text} {lhs} {rhs})")),
InfixHead::DotCallI(text) => Some(format!("(dotcall-i {lhs} {text} {rhs})")),
InfixHead::Dot => None,
}
}
fn operator_missing_rhs(op: &SyntaxToken) -> bool {
diag_count_from(
usize::from(op.text_range().start()),
DiagnosticKind::MissingOperand,
) > 0
}
fn project_range(node: &SyntaxNode) -> String {
let operands = child_nodes(node);
if operands.len() == 2 {
if node
.children_with_tokens()
.filter_map(|el| el.into_token())
.filter(|t| t.kind() == COLON)
.last()
.is_some_and(|colon| operator_missing_rhs(&colon))
{
return format!(
"(call-i {} : {} (error))",
project(&operands[0]),
project(&operands[1]),
);
}
return project_flat(significant(node));
}
if operands.len() != 3 {
return project_flat(significant(node));
}
format!(
"(call-i {} : {} {})",
project(&operands[0]),
project(&operands[1]),
project(&operands[2]),
)
}
fn project_comparison(node: &SyntaxNode) -> String {
let mut parts = Vec::new();
let mut last_op = None;
for el in significant(node) {
match el {
NodeOrToken::Node(n) => parts.push(project(&n)),
NodeOrToken::Token(t) => {
let text = t.text();
if text.starts_with('.') && text.len() > 1 && text.as_bytes()[1] != b'.' {
parts.push(format!("(. {})", &text[1..]));
} else {
parts.push(text.to_string());
}
last_op = Some(t);
}
}
}
if last_op.is_some_and(|op| operator_missing_rhs(&op)) {
parts.push("(error)".to_string());
}
sexp("comparison", parts)
}
fn project_assignment(node: &SyntaxNode) -> String {
let op = operator_token(node);
let head = match &op {
Some(t) => t.text().to_string(),
None => "=".to_string(),
};
let operands = child_nodes(node);
if operands.len() == 1 && op.is_some_and(|t| operator_missing_rhs(&t)) {
return format!("({head} {} (error))", project(&operands[0]));
}
sexp(&head, project_each(operands))
}
fn project_unary(node: &SyntaxNode) -> String {
if let Some(err) = node.children().find(|c| c.kind() == ERROR) {
let operand = child_nodes(node)
.iter()
.find(|c| c.kind() != ERROR)
.map(project)
.unwrap_or_default();
let head = if invalid_prefix_is_dotted(&err) {
"dotcall-pre"
} else {
"call-pre"
};
return format!("({head} {} {operand})", project(&err));
}
let op = match operator_token(node) {
Some(t) => t,
None => return format!("(unsupported {:?})", node.kind()),
};
let operand = project_first(node);
match op.kind() {
SUBTYPE => format!("(<:-pre {operand})"),
SUPERTYPE => format!("(>:-pre {operand})"),
AMP => format!("(& {operand})"),
DOT_PLUS => format!("(dotcall-pre + {operand})"),
DOT_MINUS => format!("(dotcall-pre - {operand})"),
DOT_TILDE => format!("(dotcall-pre ~ {operand})"),
_ => format!("(call-pre {} {operand})", op.text()),
}
}
fn invalid_prefix_is_dotted(err: &SyntaxNode) -> bool {
err.descendants_with_tokens()
.filter_map(|el| el.into_token())
.find(|t| is_operator(t.kind()))
.map(|t| {
let b = t.text().as_bytes();
b.first() == Some(&b'.') && b.len() > 1 && b[1] != b'.'
})
.unwrap_or(false)
}
fn project_postfix(node: &SyntaxNode) -> String {
let operand = project_first(node);
let op = significant(node)
.into_iter()
.filter_map(|el| el.into_token())
.find(|t| t.kind() == TRANSPOSE)
.map(|t| t.text().to_string())
.unwrap_or_else(|| "'".to_string());
format!("(call-post {operand} {op})")
}
fn project_type_annotation(node: &SyntaxNode) -> String {
let operands = child_nodes(node);
match operands.len() {
2 => format!("(::-i {} {})", project(&operands[0]), project(&operands[1])),
1 => format!("(::-pre {})", project(&operands[0])),
_ => project_flat(significant(node)),
}
}
fn project_where(node: &SyntaxNode) -> String {
let nodes = child_nodes(node);
if nodes.is_empty() {
return "(unsupported WHERE_EXPR)".to_string();
}
let mut parts = vec![project(&nodes[0])];
parts.extend(nodes[1..].iter().map(project));
sexp("where", parts)
}
fn project_dot_call(node: &SyntaxNode) -> String {
if let Some(callee) = node.children().next()
&& callee.kind() == MACRO_CALL
&& let Some(arg_list) = node.children().find(|c| c.kind() == ARG_LIST)
&& diag_at(
usize::from(arg_list.text_range().start()),
DiagnosticKind::MacroDotBroadcast,
)
{
let name = callee
.children()
.find(|c| c.kind() == MACRO_NAME)
.map(|c| project_macro_name(&c))
.unwrap_or_else(|| "@?".to_string());
let mut parts = vec![name, "(error-t)".to_string()];
parts.extend(project_args(&arg_list));
return format!("(macrocall (dotcall {}))", parts.join(" "));
}
project_call("dotcall", node)
}
fn project_call(head: &str, node: &SyntaxNode) -> String {
let mut parts = Vec::new();
let mut head = head.to_string();
for el in significant(node) {
match el {
NodeOrToken::Node(n) => {
parts.push(project(&n));
break;
}
NodeOrToken::Token(t) if head == "call" && matches!(t.kind(), SUBTYPE | SUPERTYPE) => {
head = operator_func_repr(t.kind());
break;
}
NodeOrToken::Token(t) if is_operator(t.kind()) => {
parts.push(if op_has_suffix(t.text()) {
match t.text().strip_prefix('.') {
Some(rest) if matches!(infix_head(t.kind()), InfixHead::DotCallI(_)) => {
format!("(. {rest})")
}
_ => t.text().to_string(),
}
} else {
operator_func_repr(t.kind())
});
break;
}
NodeOrToken::Token(_) => {}
}
}
if let Some(arg_list) = node.children().find(|c| c.kind() == ARG_LIST) {
if diag_at(
usize::from(arg_list.text_range().start()),
DiagnosticKind::OpenerWhitespace,
) {
parts.push("(error-t)".to_string());
}
if diag_at(
usize::from(arg_list.text_range().start()),
DiagnosticKind::PrefixOpenerWhitespace,
) {
parts.push("(error)".to_string());
}
parts.extend(project_args(&arg_list));
} else if let Some(generator) = node.children().find(|c| c.kind() == GENERATOR) {
parts.push(project_generator(&generator));
}
sexp(&head, parts)
}
fn project_typed_comprehension(node: &SyntaxNode) -> String {
let mut parts = Vec::new();
if let Some(callee) = node.children().next() {
parts.push(project(&callee));
}
if let Some(generator) = node.children().find(|c| c.kind() == GENERATOR) {
parts.push(project_generator(&generator));
}
sexp("typed_comprehension", parts)
}
fn project_args(container: &SyntaxNode) -> Vec<String> {
let mut out = Vec::new();
for el in significant(container) {
match el {
NodeOrToken::Node(n) => match n.kind() {
ARG => {
out.push(project_first(&n));
let end = usize::from(n.text_range().end());
for _ in 0..diag_count_at(end, DiagnosticKind::MatrixKeywordRecovery) {
out.push("(error-t)".to_string());
}
}
KEYWORD_ARG => out.push(project_keyword_arg(&n)),
PARAMETERS => out.push(project_parameters(&n)),
_ => out.push(project(&n)),
},
NodeOrToken::Token(t) => {
if t.kind() == END_KW {
out.push("end".to_string());
}
}
}
}
if diag_at(
usize::from(container.text_range().start()),
DiagnosticKind::UnterminatedArgList,
) {
out.push("(error-t)".to_string());
}
out
}
fn project_block_args(container: &SyntaxNode) -> Vec<String> {
let mut out = Vec::new();
let push_stmt = |n: &SyntaxNode, out: &mut Vec<String>| match n.kind() {
ARG => out.push(project_first(n)),
KEYWORD_ARG => out.push(project_keyword_arg(n)),
_ => out.push(project(n)),
};
for el in significant(container) {
if let NodeOrToken::Node(n) = el {
if n.kind() == PARAMETERS {
for inner in significant(&n) {
if let NodeOrToken::Node(m) = inner {
push_stmt(&m, &mut out);
}
}
} else {
push_stmt(&n, &mut out);
}
}
}
out
}
fn project_keyword_arg(node: &SyntaxNode) -> String {
let nodes = child_nodes(node);
match nodes.as_slice() {
[name, value] => format!("(= {} {})", project(name), project(value)),
[name] => format!("(= {})", project(name)),
_ => project_flat(significant(node)),
}
}
fn project_parameters(node: &SyntaxNode) -> String {
let mut out = Vec::new();
for el in significant(node) {
if let NodeOrToken::Node(n) = el {
match n.kind() {
ARG => out.push(project_first(&n)),
KEYWORD_ARG => out.push(project_keyword_arg(&n)),
_ => out.push(project(&n)),
}
}
}
sexp("parameters", out)
}
fn project_matrix(node: &SyntaxNode) -> String {
let (head, children) = matrix_head_and_children(node);
sexp(&head, children)
}
fn matrix_head_and_children(node: &SyntaxNode) -> (String, Vec<String>) {
let children: Vec<SyntaxNode> = node
.children()
.filter(|c| matches!(c.kind(), ARG | MATRIX_ROW))
.collect();
if children.is_empty() {
return (format!("ncat-{}", max_semicolon_run(node)), Vec::new());
}
let d = group_dimension(node);
let head = match d {
0 => "hcat".to_string(),
1 => "vcat".to_string(),
_ => format!("ncat-{d}"),
};
(head, project_cat_children(&children))
}
fn project_cat_children(children: &[SyntaxNode]) -> Vec<String> {
let mut out = Vec::new();
for child in children {
out.push(project_cat_child(child));
if child.kind() == ARG {
let end = usize::from(child.text_range().end());
for _ in 0..diag_count_at(end, DiagnosticKind::ArraySeparatorMismatch) {
out.push("(error-t)".to_string());
}
for _ in 0..diag_count_at(end, DiagnosticKind::MatrixKeywordRecovery) {
out.push("(error-t)".to_string());
}
}
}
out
}
fn project_typed_matrix(node: &SyntaxNode) -> String {
let mut children = node.children();
let type_node = children.next().expect("typed concat has a type child");
let matrix = children
.find(|c| c.kind() == MATRIX_EXPR)
.expect("typed concat has a matrix body");
let (head, mut args) = matrix_head_and_children(&matrix);
args.insert(0, project(&type_node));
sexp(&format!("typed_{head}"), args)
}
fn project_bracescat(node: &SyntaxNode) -> String {
let children: Vec<SyntaxNode> = node
.children()
.filter(|c| matches!(c.kind(), ARG | MATRIX_ROW))
.collect();
if children.is_empty() {
let d = max_semicolon_run(node);
return sexp("bracescat", vec![sexp(&format!("nrow-{d}"), Vec::new())]);
}
let d = group_dimension(node);
let items: Vec<String> = children.iter().map(project_cat_child).collect();
match d {
1 => sexp("bracescat", items),
0 => sexp("bracescat", vec![sexp("row", items)]),
_ => sexp("bracescat", vec![sexp(&format!("nrow-{d}"), items)]),
}
}
fn project_cat_child(node: &SyntaxNode) -> String {
match node.kind() {
ARG => project_first(node),
MATRIX_ROW => {
let d = group_dimension(node);
let head = if d == 0 {
"row".to_string()
} else {
format!("nrow-{d}")
};
let items: Vec<SyntaxNode> = node
.children()
.filter(|c| matches!(c.kind(), ARG | MATRIX_ROW))
.collect();
sexp(&head, project_cat_children(&items))
}
_ => project(node),
}
}
fn group_dimension(node: &SyntaxNode) -> usize {
let mut d = 0;
let mut seen_node = false;
let mut row_major = false;
let mut semis = 0usize;
let mut newline = false;
let mut newline_after_semis = false;
for el in node.children_with_tokens() {
match el {
NodeOrToken::Node(n) if matches!(n.kind(), ARG | MATRIX_ROW) => {
if seen_node {
let is_space = semis == 0 && !newline;
let continuation = semis == 2 && newline_after_semis && row_major;
let run = if continuation {
0
} else if semis > 0 {
semis
} else {
usize::from(newline)
};
if is_space {
row_major = true;
}
d = d.max(run);
}
seen_node = true;
semis = 0;
newline = false;
newline_after_semis = false;
}
NodeOrToken::Token(t) => match t.kind() {
SEMICOLON => {
semis += 1;
newline_after_semis = false;
}
NEWLINE => {
newline = true;
if semis > 0 {
newline_after_semis = true;
}
}
_ => {}
},
_ => {}
}
}
if seen_node && semis > 0 {
d = d.max(semis);
}
d
}
fn max_semicolon_run(node: &SyntaxNode) -> usize {
let mut max = 0;
let mut run = 0;
for el in node.children_with_tokens() {
match el {
NodeOrToken::Token(t) if t.kind() == SEMICOLON => {
run += 1;
max = max.max(run);
}
NodeOrToken::Token(t) if is_trivia(t.kind()) => {}
_ => run = 0,
}
}
max
}
fn project_generator(node: &SyntaxNode) -> String {
let mut body = String::new();
let mut clauses: Vec<String> = Vec::new();
for child in node.children() {
match child.kind() {
FOR_BINDING => {
if diag_at(
usize::from(child.text_range().start()),
DiagnosticKind::GluedFor,
) {
clauses.push("(error-t)".to_string());
}
clauses.push(project_for_binding_node(&child));
}
COMPREHENSION_IF => {
if let (Some(cond), Some(last)) = (first_node(&child), clauses.last().cloned()) {
let n = clauses.len();
clauses[n - 1] = format!("(filter {last} {})", project(&cond));
}
}
_ if body.is_empty() => body = project(&child),
_ => {}
}
}
let mut parts = vec![body];
parts.extend(clauses);
sexp("generator", parts)
}
fn project_for_binding(node: &SyntaxNode) -> String {
match node.children().find(|c| c.kind() == FOR_BINDING) {
Some(binding) => project_for_binding_node(&binding),
None => "(unsupported FOR_BINDING)".to_string(),
}
}
fn project_for_binding_node(binding: &SyntaxNode) -> String {
let mut specs: Vec<Vec<SyntaxElement>> = vec![Vec::new()];
for el in binding.children_with_tokens() {
match &el {
NodeOrToken::Token(t) if t.kind() == COMMA => specs.push(Vec::new()),
NodeOrToken::Token(t) if is_drop_token(t.kind()) => {}
_ => specs.last_mut().expect("non-empty").push(el),
}
}
let projected: Vec<String> = specs.iter().map(|s| project_for_spec(s)).collect();
match projected.as_slice() {
[one] => one.clone(),
_ => sexp("cartesian_iterator", projected),
}
}
fn project_for_spec(elems: &[SyntaxElement]) -> String {
if let [NodeOrToken::Node(n)] = elems
&& n.kind() == ASSIGNMENT_EXPR
{
return project(n);
}
let split = elems
.iter()
.position(|el| matches!(el, NodeOrToken::Token(t) if t.text() == "in" || t.text() == "∈"));
match split {
Some(idx) => {
let var = project_flat(elems[..idx].to_vec());
let iter = project_flat(elems[idx + 1..].to_vec());
format!("(= {var} {iter})")
}
None => project_flat(elems.to_vec()),
}
}
fn missing_condition(node: &SyntaxNode) -> String {
if diag_count_from(keyword_start(node), DiagnosticKind::MissingCondition) > 0 {
"(error)".to_string()
} else {
String::new()
}
}
fn project_if(node: &SyntaxNode) -> String {
let cond = node
.children()
.find(|c| c.kind() == CONDITION)
.map(|c| project(&c))
.unwrap_or_else(|| missing_condition(node));
let then_block = node
.children()
.find(|c| c.kind() == BLOCK)
.map(|c| project(&c))
.unwrap_or_else(|| "(block)".to_string());
let clauses: Vec<SyntaxNode> = node
.children()
.filter(|c| matches!(c.kind(), ELSEIF_CLAUSE | ELSE_CLAUSE))
.collect();
let mut parts = vec![cond, then_block];
if let Some(err) = node.children().find(|c| c.kind() == ERROR) {
parts.push(project(&err));
}
for _ in 0..diag_count_from(keyword_start(node), DiagnosticKind::ElseIf) {
parts.push("(error-t)".to_string());
}
if let Some(tail) = project_if_tail(&clauses) {
parts.push(tail);
}
push_trailing_errors(node, &mut parts);
sexp("if", parts)
}
fn project_if_tail(clauses: &[SyntaxNode]) -> Option<String> {
let first = clauses.first()?;
match first.kind() {
ELSE_CLAUSE => first
.children()
.find(|c| c.kind() == BLOCK)
.map(|c| project(&c)),
ELSEIF_CLAUSE => {
let cond = first
.children()
.find(|c| c.kind() == CONDITION)
.map(|c| project(&c))
.unwrap_or_else(|| missing_condition(first));
let block = first
.children()
.find(|c| c.kind() == BLOCK)
.map(|c| project(&c))
.unwrap_or_else(|| "(block)".to_string());
let mut parts = vec![cond, block];
if let Some(tail) = project_if_tail(&clauses[1..]) {
parts.push(tail);
}
Some(sexp("elseif", parts))
}
_ => None,
}
}
fn project_try(node: &SyntaxNode) -> String {
let mut parts = Vec::new();
parts.push(project_block_child(node));
for clause in node.children() {
match clause.kind() {
CATCH_CLAUSE => {
let var = clause
.children()
.find(|c| c.kind() != BLOCK)
.map(|c| {
let projected = project(&c);
if diag_at(
usize::from(c.text_range().start()),
DiagnosticKind::CatchVarNotIdentifier,
) {
format!("(error {projected})")
} else {
projected
}
})
.unwrap_or_else(|| "false".to_string());
let block = project_block_child(&clause);
parts.push(format!("(catch {var} {block})"));
}
FINALLY_CLAUSE => parts.push(format!("(finally {})", project_block_child(&clause))),
ELSE_CLAUSE => {
if let Some(err) = clause.children().find(|c| c.kind() == ERROR) {
parts.push(format!("(else {})", project(&err)));
} else {
parts.push(format!("(else {})", project_block_child(&clause)));
}
}
_ => {}
}
}
let kw = keyword_start(node);
for _ in 0..diag_count_from(kw, DiagnosticKind::MissingTryHandler) {
parts.push("(error-t)".to_string());
}
for _ in 0..diag_count_from(kw, DiagnosticKind::MissingEnd) {
parts.push("(error-t)".to_string());
}
sexp("try", parts)
}
fn project_struct(node: &SyntaxNode) -> String {
let mutable = node
.children_with_tokens()
.any(|el| el.kind() == MUTABLE_KW);
let head = if mutable { "struct-mut" } else { "struct" };
let mut parts = vec![project_signature(node), project_block_child(node)];
push_trailing_errors(node, &mut parts);
sexp(head, parts)
}
fn project_primitive(node: &SyntaxNode) -> String {
let spec = project_signature(node);
let size = node
.children()
.find(|c| c.kind() != SIGNATURE)
.map(|c| project(&c))
.unwrap_or_default();
sexp("primitive", vec![spec, size])
}
fn project_module(node: &SyntaxNode) -> String {
let bare = node
.children_with_tokens()
.any(|el| el.kind() == BAREMODULE_KW);
let head = if bare { "module-bare" } else { "module" };
let mut parts = vec![project_signature(node), project_block_child(node)];
push_trailing_errors(node, &mut parts);
sexp(head, parts)
}
fn project_quote_sym(node: &SyntaxNode) -> String {
let mut prefix = "";
for el in node.children_with_tokens() {
match el {
NodeOrToken::Node(n) => return format!("(quote-: {prefix}{})", project(&n)),
NodeOrToken::Token(t) if t.kind() == COLON => {
if diag_at(
usize::from(t.text_range().end()),
DiagnosticKind::QuoteColonWhitespace,
) {
prefix = "(error-t) ";
}
}
NodeOrToken::Token(t)
if t.kind() == LPAREN
&& diag_at(
usize::from(t.text_range().end()),
DiagnosticKind::EmptyQuoteParen,
) =>
{
return format!("(quote-: {prefix}(error-t))");
}
NodeOrToken::Token(t) if is_trivia(t.kind()) => continue,
NodeOrToken::Token(t) => return format!("(quote-: {prefix}{})", t.text()),
}
}
"(quote-:)".to_string()
}
fn project_let(node: &SyntaxNode) -> String {
let bindings = match node.children().find(|c| c.kind() == LET_BINDINGS) {
Some(b) => sexp("block", project_let_bindings(&b)),
None => "(block)".to_string(),
};
let mut parts = vec![bindings, project_block_child(node)];
push_trailing_errors(node, &mut parts);
sexp("let", parts)
}
fn project_let_bindings(node: &SyntaxNode) -> Vec<String> {
node.children().map(|c| project(&c)).collect()
}
fn project_do(node: &SyntaxNode) -> String {
let call = node
.children()
.next()
.map(|c| project(&c))
.unwrap_or_default();
let params = match node.children().find(|c| c.kind() == DO_PARAMS) {
Some(p) => sexp("tuple", do_param_strings(&p)),
None => "(tuple)".to_string(),
};
let block = project_block_child(node);
let mut parts = vec![call, params, block];
push_trailing_errors(node, &mut parts);
sexp("do", parts)
}
fn do_param_strings(node: &SyntaxNode) -> Vec<String> {
significant(node)
.into_iter()
.filter_map(|el| match el {
NodeOrToken::Node(n) => Some(project(&n)),
NodeOrToken::Token(t) if t.kind() == IDENT => Some(t.text().to_string()),
NodeOrToken::Token(_) => None,
})
.collect()
}
fn project_keyword_stmt(head: &str, node: &SyntaxNode) -> String {
match first_node(node) {
Some(inner) => format!("({head} {})", project(&inner)),
None => format!("({head})"),
}
}
fn project_decl(head: &str, node: &SyntaxNode) -> String {
let nodes = child_nodes(node);
if let [only] = nodes.as_slice()
&& only.kind() == BARE_TUPLE_EXPR
{
return sexp(head, project_each(child_nodes(only)));
}
let items: Vec<String> = significant(node)
.into_iter()
.filter_map(|el| match el {
NodeOrToken::Node(n) => Some(project(&n)),
NodeOrToken::Token(t) if t.kind() == IDENT => Some(t.text().to_string()),
NodeOrToken::Token(_) => None,
})
.collect();
sexp(head, items)
}
fn project_export(node: &SyntaxNode) -> String {
let items: Vec<String> = significant(node)
.into_iter()
.filter_map(|el| match el {
NodeOrToken::Node(n) if matches!(n.kind(), PAREN_EXPR | TUPLE_EXPR) => {
let projected = project(&n);
if diag_at(
usize::from(n.text_range().start()),
DiagnosticKind::InvalidExportItem,
) {
Some(format!("(error {projected})"))
} else {
Some(projected)
}
}
_ => name_run_item(el),
})
.collect();
sexp("export", items)
}
fn project_public(node: &SyntaxNode) -> String {
let items: Vec<String> = node
.children_with_tokens()
.filter(|el| match el {
NodeOrToken::Node(_) => true,
NodeOrToken::Token(t) => !is_trivia(t.kind()) && t.kind() != COMMA,
})
.skip(1)
.filter_map(name_run_item)
.collect();
sexp("public", items)
}
fn project_import(head: &str, node: &SyntaxNode) -> String {
let mut first_sep: Option<SyntaxKind> = None;
let mut clauses: Vec<String> = Vec::new();
for el in node.children_with_tokens() {
match el {
NodeOrToken::Node(n) if matches!(n.kind(), IMPORT_PATH | IMPORT_ALIAS | ERROR) => {
clauses.push(project(&n));
}
NodeOrToken::Token(t)
if matches!(t.kind(), COLON | COMMA)
&& !clauses.is_empty()
&& first_sep.is_none() =>
{
first_sep = Some(t.kind());
}
_ => {}
}
}
if first_sep == Some(COLON) && !clauses.is_empty() {
format!("({head} {})", sexp(":", clauses))
} else {
format!("({head} {})", clauses.join(" "))
}
}
fn project_import_path(node: &SyntaxNode) -> String {
let mut parts: Vec<String> = Vec::new();
let mut seen_name = false;
for el in node.children_with_tokens() {
match el {
NodeOrToken::Token(t) => match t.kind() {
DOT if !seen_name => parts.push(".".to_string()),
DOT_DOT if !seen_name => parts.extend([".".to_string(), ".".to_string()]),
DOT_DOT_DOT if !seen_name => {
parts.extend([".".to_string(), ".".to_string(), ".".to_string()])
}
IDENT => {
parts.push(t.text().to_string());
seen_name = true;
}
DOT_DOT_DOT if seen_name => parts.push("..".to_string()),
DOT | DOT_DOT | DOT_DOT_DOT | COLON => {}
k if is_operator(k) => {
if !seen_name && t.text().starts_with('.') {
parts.push(".".to_string());
}
parts.push(t.text().trim_start_matches('.').to_string());
seen_name = true;
}
_ => {}
},
NodeOrToken::Node(n) if n.kind() == NAME => {
parts.push(name_text(&n));
seen_name = true;
}
NodeOrToken::Node(n) if n.kind() == QUOTE_SYM => {
parts.push(project_quote_sym(&n));
seen_name = true;
}
NodeOrToken::Node(n) if n.kind() == PAREN_EXPR => {
parts.push(project(&n));
seen_name = true;
}
NodeOrToken::Node(n) if n.kind() == INTERPOLATION => {
parts.push(project(&n));
seen_name = true;
}
NodeOrToken::Node(n) if n.kind() == MACRO_NAME => {
parts.push(project_macro_name(&n));
seen_name = true;
}
_ => {}
}
}
sexp("importpath", parts)
}
fn project_import_alias(node: &SyntaxNode) -> String {
let path = node
.children()
.find(|c| c.kind() == IMPORT_PATH)
.map(|c| project_import_path(&c))
.unwrap_or_default();
let alias = node
.children_with_tokens()
.filter_map(|el| match el {
NodeOrToken::Token(t) if t.kind() == IDENT && t.text() != "as" => {
Some(t.text().to_string())
}
_ => None,
})
.last()
.unwrap_or_default();
format!("(as {path} {alias})")
}
fn project_macrocall(node: &SyntaxNode) -> String {
let name = node
.children()
.find(|c| c.kind() == MACRO_NAME)
.map(|c| project_macro_name(&c))
.unwrap_or_else(|| "@?".to_string());
if let Some(arg_list) = node.children().find(|c| c.kind() == ARG_LIST) {
let mut parts = vec![name];
parts.extend(project_args(&arg_list));
return sexp("macrocall-p", parts);
}
let mut parts = vec![name];
parts.extend(
node.children()
.filter(|c| c.kind() != MACRO_NAME)
.map(|c| project(&c)),
);
sexp("macrocall", parts)
}
fn project_macro_name(node: &SyntaxNode) -> String {
if let Some(child) = node.children().next()
&& diag_at(
usize::from(child.text_range().start()),
DiagnosticKind::InvalidMacroName,
)
{
return format!("(error {})", project(&child));
}
let var_name = node
.children()
.find(|c| c.kind() == NONSTANDARD_IDENTIFIER)
.map(|c| format!("(var @{})", raw_content(&c)));
if let Some(module) = node
.children()
.find(|c| matches!(c.kind(), NAME | BINARY_EXPR | INTERPOLATION))
{
if var_name.is_none()
&& let Some(at_start) = node
.children_with_tokens()
.filter_map(|el| el.into_token())
.find(|t| t.kind() == AT)
.map(|t| usize::from(t.text_range().start()))
&& diag_at(at_start, DiagnosticKind::MacroSigilTrailing)
{
let mut after_at = false;
let comps: Vec<String> = node
.children_with_tokens()
.filter_map(|el| el.into_token())
.filter_map(|t| {
if t.kind() == AT {
after_at = true;
return None;
}
if after_at && (t.kind() == IDENT || is_macro_name_part_token(t.kind())) {
Some(t.text().to_string())
} else {
None
}
})
.collect();
if let [first, rest @ ..] = comps.as_slice() {
let mut path = format!("(. {} (quote {first}))", project(&module));
if let Some((last, mids)) = rest.split_last() {
for c in mids {
path = format!("(. {path} (error-t) (quote {c}))");
}
path = format!("(. {path} (error-t) (quote @{last}))");
}
return path;
}
}
let name = match &var_name {
Some(v) => v.clone(),
None => format!("@{}", macro_name_after_at(node)),
};
return format!("(. {} (quote {name}))", project(&module));
}
if let Some(v) = var_name {
return v;
}
let sig_toks: Vec<SyntaxToken> = node
.children_with_tokens()
.filter_map(|el| el.into_token())
.filter(|t| matches!(t.kind(), IDENT | DOT | DOLLAR | AT))
.collect();
if sig_toks
.windows(2)
.any(|w| w[0].kind() == DOT && matches!(w[1].kind(), DOLLAR | AT))
{
return project_leading_macro_path(&sig_toks);
}
let comps: Vec<String> = node
.children_with_tokens()
.filter_map(|el| match el {
NodeOrToken::Token(t) if t.kind() == IDENT => Some(t.text().to_string()),
NodeOrToken::Token(t) if is_macro_name_part_token(t.kind()) => {
Some(t.text().to_string())
}
_ => None,
})
.collect();
match comps.as_slice() {
[] => "@.".to_string(),
[one] => format!("@{one}"),
rest => {
let (macro_name, module) = rest.split_last().unwrap();
let mut path = module[0].clone();
for c in &module[1..] {
path = format!("(. {path} (quote {c}))");
}
format!("(. {path} (quote @{macro_name}))")
}
}
}
fn project_leading_macro_path(sig_toks: &[SyntaxToken]) -> String {
let at_start = usize::from(sig_toks[0].text_range().start());
let invalid = diag_at(at_start, DiagnosticKind::MacroSigilLeading);
let mut comps: Vec<(SyntaxKind, String)> = Vec::new();
let mut i = 1;
if sig_toks.get(i).map(|t| t.kind()) == Some(IDENT) {
comps.push((IDENT, sig_toks[i].text().to_string()));
i += 1;
}
while sig_toks.get(i).map(|t| t.kind()) == Some(DOT) {
i += 1; match sig_toks.get(i).map(|t| t.kind()) {
Some(IDENT) => {
comps.push((IDENT, sig_toks[i].text().to_string()));
i += 1;
}
Some(k @ (DOLLAR | AT)) => {
let name = sig_toks
.get(i + 1)
.map(|t| t.text().to_string())
.unwrap_or_default();
comps.push((k, name));
i += 2;
}
_ => break,
}
}
let Some(((_, root), steps)) = comps.split_first() else {
return "@.".to_string();
};
let last = steps.len().saturating_sub(1);
let inner_at = steps[..last].iter().any(|(k, _)| *k == AT);
let mut path = root.clone();
for (idx, (kind, name)) in steps.iter().enumerate() {
let is_final = idx == last;
let step = match (*kind, is_final) {
(DOLLAR, true) if invalid => format!("(inert (error {name}))"),
(DOLLAR, _) => format!("(inert ($ {name}))"),
(AT, true) if invalid => format!("(quote (error-t) @{name})"),
(AT, false) if invalid => format!("(quote (error-t) {name})"),
(_, true) if invalid && inner_at => format!("(error-t) (quote @{name})"),
(_, true) => format!("(quote @{name})"),
(_, false) => format!("(quote {name})"),
};
path = format!("(. {path} {step})");
}
path
}
fn macro_name_after_at(node: &SyntaxNode) -> String {
let mut after_at = false;
for el in node.children_with_tokens() {
if let NodeOrToken::Token(t) = el {
if after_at {
return t.text().to_string();
}
if t.kind() == AT {
after_at = true;
}
}
}
String::new()
}
fn is_macro_name_part_token(kind: SyntaxKind) -> bool {
(is_operator(kind) && kind != DOT) || kind == DOLLAR || is_keyword(kind)
}
fn project_literal(node: &SyntaxNode) -> String {
let toks: Vec<_> = node
.children_with_tokens()
.filter_map(|el| el.into_token())
.collect();
if let [sign, num] = toks.as_slice() {
let n = literal_token_text(num);
return if sign.text() == "-" {
format!("-{n}")
} else {
n
};
}
match toks.first() {
Some(tok) => literal_token_text(tok),
None => "(unsupported LITERAL)".to_string(),
}
}
fn literal_token_text(tok: &SyntaxToken) -> String {
match tok.kind() {
CHAR => project_char(tok),
TRUE_KW => "true".to_string(),
FALSE_KW => "false".to_string(),
INTEGER => tok.text().replace('_', ""),
HEX_INT => normalize_based_int(tok.text(), 16),
OCT_INT => normalize_based_int(tok.text(), 8),
BIN_INT => normalize_based_int(tok.text(), 2),
_ => tok.text().to_string(),
}
}
fn normalize_based_int(text: &str, base: u32) -> String {
let digits: String = text[2..].chars().filter(|&c| c != '_').collect();
let nbits = match base {
16 => 4 * digits.len(),
2 => digits.len(),
_ => octal_bits(&digits),
};
let tier_bits = match nbits {
0..=8 => 8,
9..=16 => 16,
17..=32 => 32,
33..=64 => 64,
65..=128 => 128,
_ => return text.replace('_', ""),
};
match u128::from_str_radix(&digits, base) {
Ok(v) => format!("0x{:0width$x}", v, width = tier_bits / 4),
Err(_) => text.replace('_', ""),
}
}
fn octal_bits(digits: &str) -> usize {
let lead = digits.as_bytes()[0] - b'0';
let lead_bits = (8 - lead.leading_zeros()) as usize;
lead_bits + 3 * (digits.len() - 1)
}
fn project_char(tok: &SyntaxToken) -> String {
let text = tok.text();
let start = usize::from(tok.text_range().start());
if diag_at(start, DiagnosticKind::UnterminatedLiteral) {
let inner = text.strip_prefix('\'').unwrap_or(text);
if inner.is_empty() {
return "(char (error))".to_string();
}
return match decode_char_body(inner) {
Some(c) => format!("(char '{}' (error-t))", display_char(c)),
None => match classify_char_body(inner) {
CharError::Empty => "(char (error))".to_string(),
CharError::BadEscape => "(char (ErrorInvalidEscapeSequence) (error-t))".to_string(),
CharError::OverLong => "(char (ErrorOverLongCharacter) (error-t))".to_string(),
CharError::SingleByte(b) => format!("(char '\\x{b:02x}' (error-t))"),
},
};
}
match decode_char(text) {
Some(c) => format!("(char '{}')", display_char(c)),
None => match classify_char_error(text) {
CharError::Empty => "(char (error))".to_string(),
CharError::BadEscape => "(char (ErrorInvalidEscapeSequence))".to_string(),
CharError::OverLong => "(char (ErrorOverLongCharacter))".to_string(),
CharError::SingleByte(b) => format!("(char '\\x{b:02x}')"),
},
}
}
enum CharError {
Empty,
BadEscape,
SingleByte(u8),
OverLong,
}
fn classify_char_error(text: &str) -> CharError {
let inner = text
.strip_prefix('\'')
.and_then(|s| s.strip_suffix('\''))
.unwrap_or("");
classify_char_body(inner)
}
fn classify_char_body(inner: &str) -> CharError {
if inner.is_empty() {
return CharError::Empty;
}
let mut bytes: Vec<u8> = Vec::new();
let mut buf = [0u8; 4];
let mut chars = inner.chars().peekable();
while let Some(c) = chars.next() {
if c != '\\' {
bytes.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
} else if decode_escape_into(&mut chars, &mut bytes).is_none() {
return CharError::BadEscape;
}
}
match bytes.as_slice() {
[b] => CharError::SingleByte(*b),
_ => CharError::OverLong,
}
}
fn decode_char(text: &str) -> Option<char> {
let inner = text.strip_prefix('\'')?.strip_suffix('\'')?;
decode_char_body(inner)
}
fn decode_char_body(inner: &str) -> Option<char> {
if inner.is_empty() {
return None;
}
let mut bytes: Vec<u8> = Vec::new();
let mut buf = [0u8; 4];
let mut chars = inner.chars().peekable();
while let Some(c) = chars.next() {
if c != '\\' {
bytes.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
continue;
}
decode_escape_into(&mut chars, &mut bytes)?;
}
let s = std::str::from_utf8(&bytes).ok()?;
let mut it = s.chars();
let first = it.next()?;
it.next().is_none().then_some(first)
}
fn decode_escape_into(
chars: &mut std::iter::Peekable<std::str::Chars>,
bytes: &mut Vec<u8>,
) -> Option<()> {
let mut buf = [0u8; 4];
match chars.next()? {
'n' => bytes.push(b'\n'),
't' => bytes.push(b'\t'),
'r' => bytes.push(b'\r'),
'a' => bytes.push(0x07),
'b' => bytes.push(0x08),
'f' => bytes.push(0x0c),
'v' => bytes.push(0x0b),
'e' => bytes.push(0x1b),
'\\' => bytes.push(b'\\'),
'\'' => bytes.push(b'\''),
'"' => bytes.push(b'"'),
'$' => bytes.push(b'$'),
'x' => bytes.push(take_hex(chars, 2)? as u8),
'u' => {
let cp = char::from_u32(take_hex(chars, 4)?)?;
bytes.extend_from_slice(cp.encode_utf8(&mut buf).as_bytes());
}
'U' => {
let cp = char::from_u32(take_hex(chars, 8)?)?;
bytes.extend_from_slice(cp.encode_utf8(&mut buf).as_bytes());
}
d @ '0'..='7' => {
let mut val = d.to_digit(8)?;
for _ in 0..2 {
match chars.peek().and_then(|c| c.to_digit(8)) {
Some(o) => {
val = val * 8 + o;
chars.next();
}
None => break,
}
}
bytes.push(u8::try_from(val).ok()?);
}
_ => return None,
}
Some(())
}
fn take_hex(chars: &mut std::iter::Peekable<std::str::Chars>, max: usize) -> Option<u32> {
let mut val = 0u32;
let mut n = 0;
while n < max {
match chars.peek().and_then(|c| c.to_digit(16)) {
Some(d) => {
val = val * 16 + d;
chars.next();
n += 1;
}
None => break,
}
}
(n > 0).then_some(val)
}
fn control_escape(c: char) -> Option<String> {
Some(match c {
'\0' => "\\0".to_string(),
'\u{7}' => "\\a".to_string(),
'\u{8}' => "\\b".to_string(),
'\t' => "\\t".to_string(),
'\n' => "\\n".to_string(),
'\u{b}' => "\\v".to_string(),
'\u{c}' => "\\f".to_string(),
'\r' => "\\r".to_string(),
'\u{1b}' => "\\e".to_string(),
c if (c as u32) < 0x20 || c as u32 == 0x7f => format!("\\x{:02x}", c as u32),
c if c.is_control() => {
let v = c as u32;
if v <= 0xffff {
format!("\\u{v:04x}")
} else {
format!("\\U{v:08x}")
}
}
_ => return None,
})
}
fn display_char(c: char) -> String {
match c {
'\\' => "\\\\".to_string(),
'\'' => "\\'".to_string(),
c => control_escape(c).unwrap_or_else(|| c.to_string()),
}
}
fn project_string(node: &SyntaxNode) -> String {
if let Some(prefix) = string_token(node, STRING_PREFIX) {
let body = if matches!(string_token(node, STRING_DELIM_OPEN), Some(d) if d.len() >= 3) {
sexp(
"string-s-r",
with_error_trivia(node, triple_string_parts(node, true)),
)
} else {
sexp(
"string-r",
with_error_trivia(node, vec![quote_raw(&raw_content(node))]),
)
};
let mut parts = vec![format!("@{prefix}_str"), body];
if let Some(suffix) = string_token(node, STRING_SUFFIX) {
parts.push(quote_raw(&suffix));
} else if let Some(num) = numeric_suffix(node) {
parts.push(num);
}
return sexp("macrocall", parts);
}
if matches!(string_token(node, STRING_DELIM_OPEN), Some(d) if d.len() >= 3) {
return sexp(
"string-s",
with_error_trivia(node, triple_string_parts(node, false)),
);
}
let mut parts = string_parts(node);
if parts.is_empty() {
parts.push("\"\"".to_string());
}
sexp("string", with_error_trivia(node, parts))
}
enum TripleItem {
Text(String),
Interp(String),
}
fn triple_string_parts(node: &SyntaxNode, raw: bool) -> Vec<String> {
let mut lines: Vec<Vec<TripleItem>> = vec![Vec::new()];
for el in node.children_with_tokens() {
match el {
NodeOrToken::Token(t) if t.kind() == STRING_CONTENT => {
let text = normalize_newlines(t.text());
let mut segs = text.split('\n');
if let Some(first) = segs.next() {
lines
.last_mut()
.unwrap()
.push(TripleItem::Text(first.to_string()));
}
for seg in segs {
lines.push(vec![TripleItem::Text(seg.to_string())]);
}
}
NodeOrToken::Node(n) if n.kind() == INTERPOLATION => {
lines
.last_mut()
.unwrap()
.push(TripleItem::Interp(project_interpolation(&n)));
}
_ => {}
}
}
chunk_triple_lines(lines, raw)
}
fn triple_cmd_parts(node: &SyntaxNode) -> Vec<String> {
let body = normalize_newlines(&cmd_raw_body(node));
let lines: Vec<Vec<TripleItem>> = body
.split('\n')
.map(|seg| vec![TripleItem::Text(seg.to_string())])
.collect();
chunk_triple_lines(lines, true)
}
fn chunk_triple_lines(lines: Vec<Vec<TripleItem>>, raw: bool) -> Vec<String> {
let last_idx = lines.len() - 1;
let candidates: Vec<String> = lines
.iter()
.enumerate()
.filter(|(i, line)| *i != 0 && (!line_is_blank(line) || *i == last_idx))
.map(|(_, line)| line_lead_ws(line))
.collect();
let dedent_len = common_prefix_len(&candidates);
let mut chunks: Vec<TripleItem> = Vec::new();
for (i, mut line) in lines.into_iter().enumerate() {
let has_newline = i != last_idx;
if i == 0 {
if line_is_empty(&line) {
continue;
}
} else if let Some(TripleItem::Text(t)) = line.first_mut() {
*t = strip_leading_ws(t, dedent_len);
}
if has_newline {
match line.last_mut() {
Some(TripleItem::Text(t)) => t.push('\n'),
_ => line.push(TripleItem::Text("\n".to_string())),
}
}
chunks.extend(line);
}
let mut out: Vec<String> = Vec::new();
for chunk in chunks {
match chunk {
TripleItem::Text(t) if t.is_empty() => {}
TripleItem::Text(t) => out.push(format!("\"{}\"", escape_display(&t, raw))),
TripleItem::Interp(s) => out.push(s),
}
}
if out.is_empty() {
out.push("\"\"".to_string());
}
out
}
fn normalize_newlines(s: &str) -> String {
s.replace("\r\n", "\n").replace('\r', "\n")
}
fn line_lead_ws(line: &[TripleItem]) -> String {
match line.first() {
Some(TripleItem::Text(t)) => t.chars().take_while(|c| *c == ' ' || *c == '\t').collect(),
_ => String::new(),
}
}
fn line_is_blank(line: &[TripleItem]) -> bool {
line.iter().all(|it| match it {
TripleItem::Text(t) => t.chars().all(|c| c == ' ' || c == '\t'),
TripleItem::Interp(_) => false,
})
}
fn line_is_empty(line: &[TripleItem]) -> bool {
line.iter()
.all(|it| matches!(it, TripleItem::Text(t) if t.is_empty()))
}
fn common_prefix_len(strs: &[String]) -> usize {
let mut iter = strs.iter();
let Some(first) = iter.next() else {
return 0;
};
let mut len = first.len();
for s in iter {
len = first
.bytes()
.zip(s.bytes())
.take(len)
.take_while(|(a, b)| a == b)
.count();
}
len
}
fn strip_leading_ws(t: &str, n: usize) -> String {
let mut idx = 0;
for c in t.chars() {
if idx >= n || (c != ' ' && c != '\t') {
break;
}
idx += 1;
}
t[idx..].to_string()
}
fn escape_display(s: &str, raw: bool) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'\n' => out.push_str("\\n"),
'\t' => out.push_str("\\t"),
'\r' => out.push_str("\\r"),
'\\' if raw => out.push_str("\\\\"),
'"' if raw => out.push_str("\\\""),
'$' if raw => out.push_str("\\$"),
_ => out.push(c),
}
}
out
}
fn project_var(node: &SyntaxNode) -> String {
let content = unescape_raw_string(&raw_content(node));
let parts = if content.is_empty() {
vec![]
} else {
vec![content]
};
sexp("var", with_error_trivia(node, parts))
}
fn unescape_raw_string(s: &str) -> String {
let bytes = s.as_bytes();
let mut out = String::with_capacity(s.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'\\' {
let mut run = 0;
while i + run < bytes.len() && bytes[i + run] == b'\\' {
run += 1;
}
let at_end = i + run >= bytes.len();
let before_quote = !at_end && bytes[i + run] == b'"';
if before_quote || at_end {
for _ in 0..run / 2 {
out.push('\\');
}
i += run;
if run % 2 == 1 {
out.push('"');
i += 1;
}
} else {
for _ in 0..run {
out.push('\\');
}
i += run;
}
} else {
let ch = s[i..].chars().next().unwrap();
out.push(ch);
i += ch.len_utf8();
}
}
out
}
fn project_cmd(node: &SyntaxNode) -> String {
let triple = matches!(string_token(node, CMD_DELIM_OPEN), Some(d) if d.len() >= 3);
let head = match string_token(node, STRING_PREFIX) {
Some(prefix) => format!("@{prefix}_cmd"),
None => "core_@cmd".to_string(),
};
let body = if triple {
sexp(
"cmdstring-s-r",
with_error_trivia(node, triple_cmd_parts(node)),
)
} else {
sexp(
"cmdstring-r",
with_error_trivia(node, vec![quote_raw(&cmd_raw_body(node))]),
)
};
let mut parts = vec![head, body];
if let Some(suffix) = string_token(node, STRING_SUFFIX) {
parts.push(quote_raw(&suffix));
}
sexp("macrocall", parts)
}
fn cmd_raw_body(node: &SyntaxNode) -> String {
let mut body = String::new();
for el in node.children_with_tokens() {
match el {
NodeOrToken::Token(t) if t.kind() == STRING_CONTENT => body.push_str(t.text()),
NodeOrToken::Node(n) if n.kind() == INTERPOLATION => {
body.push_str(&n.text().to_string())
}
_ => {}
}
}
body
}
fn string_parts(node: &SyntaxNode) -> Vec<String> {
decoded_string_parts(node).unwrap_or_else(|| raw_string_parts(node))
}
fn decoded_string_parts(node: &SyntaxNode) -> Option<Vec<String>> {
let mut parts = Vec::new();
for el in node.children_with_tokens() {
match el {
NodeOrToken::Token(t) if t.kind() == STRING_CONTENT => {
match decode_string_chunks(t.text()) {
Ok(chunks) => {
for chunk in chunks {
if !chunk.is_empty() {
parts.push(format!("\"{}\"", escape_string_value(&chunk)));
}
}
}
Err(StringDecodeError::BadEscape) => {
parts.push("(ErrorInvalidEscapeSequence)".to_string());
}
Err(StringDecodeError::BadUtf8) => return None,
}
}
NodeOrToken::Node(n) if n.kind() == INTERPOLATION => {
parts.push(project_interpolation(&n));
}
_ => {}
}
}
Some(parts)
}
enum StringDecodeError {
BadEscape,
BadUtf8,
}
fn raw_string_parts(node: &SyntaxNode) -> Vec<String> {
let mut parts = Vec::new();
for el in node.children_with_tokens() {
match el {
NodeOrToken::Token(t) if t.kind() == STRING_CONTENT => {
parts.push(format!("\"{}\"", t.text()));
}
NodeOrToken::Node(n) if n.kind() == INTERPOLATION => {
parts.push(project_interpolation(&n));
}
_ => {}
}
}
parts
}
fn decode_string_chunks(text: &str) -> Result<Vec<String>, StringDecodeError> {
let mut chunks: Vec<Vec<u8>> = vec![Vec::new()];
let mut buf = [0u8; 4];
let mut chars = text.chars().peekable();
while let Some(c) = chars.next() {
if c != '\\' {
let last = chunks.last_mut().unwrap();
last.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
continue;
}
match chars.peek() {
Some('\n') | Some('\r') => {
let nl = chars.next().unwrap();
if nl == '\r' && chars.peek() == Some(&'\n') {
chars.next();
}
while matches!(chars.peek(), Some(' ') | Some('\t')) {
chars.next();
}
chunks.push(Vec::new());
}
_ => decode_escape_into(&mut chars, chunks.last_mut().unwrap())
.ok_or(StringDecodeError::BadEscape)?,
}
}
chunks
.into_iter()
.map(|c| String::from_utf8(c).map_err(|_| StringDecodeError::BadUtf8))
.collect()
}
fn escape_string_value(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'$' => out.push_str("\\$"),
c => match control_escape(c) {
Some(esc) => out.push_str(&esc),
None => out.push(c),
},
}
}
out
}
fn project_interpolation(node: &SyntaxNode) -> String {
if let Some(inner) = first_node(node) {
return match inner.kind() {
PAREN_BLOCK => sexp("error", project_block_args(&inner)),
TUPLE_EXPR => sexp("error", project_args(&inner)),
GENERATOR => sexp("error", vec![project_generator(&inner)]),
_ => project(&inner),
};
}
node.children_with_tokens()
.filter_map(|el| el.into_token())
.find(|t| t.kind() == IDENT)
.map(|t| t.text().to_string())
.unwrap_or_default()
}
fn raw_content(node: &SyntaxNode) -> String {
node.children_with_tokens()
.filter_map(|el| el.into_token())
.filter(|t| t.kind() == STRING_CONTENT)
.map(|t| t.text().to_string())
.collect()
}
fn numeric_suffix(node: &SyntaxNode) -> Option<String> {
node.children_with_tokens()
.filter_map(|el| el.into_token())
.find(|t| matches!(t.kind(), INTEGER | FLOAT | FLOAT32))
.map(|t| t.text().to_string())
}
fn string_token(node: &SyntaxNode, kind: SyntaxKind) -> Option<String> {
node.children_with_tokens()
.filter_map(|el| el.into_token())
.find(|t| t.kind() == kind)
.map(|t| t.text().to_string())
}
fn quote_raw(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'$' => out.push_str("\\$"),
_ => out.push(c),
}
}
out.push('"');
out
}
fn project_function_like(head: &str, node: &SyntaxNode) -> String {
if is_forward_declaration(node) {
return sexp(head, vec![project_signature(node)]);
}
let sig = if !node.children().any(|c| c.kind() == SIGNATURE) {
"(error (error))".to_string()
} else if invalid_bare_signature(node) {
format!("(error {})", project_signature(node))
} else {
project_signature(node)
};
let mut parts = vec![sig, project_block_child(node)];
push_trailing_errors(node, &mut parts);
sexp(head, parts)
}
fn is_forward_declaration(node: &SyntaxNode) -> bool {
signature_is_bare_name(node) && !invalid_bare_signature(node)
}
fn signature_is_bare_name(node: &SyntaxNode) -> bool {
node.children()
.find(|c| c.kind() == SIGNATURE)
.and_then(|sig| first_node(&sig))
.map(|inner| matches!(inner.kind(), NAME | INTERPOLATION))
.unwrap_or(false)
}
fn invalid_bare_signature(node: &SyntaxNode) -> bool {
node.children()
.find(|c| c.kind() == SIGNATURE)
.map(|sig| {
diag_at(
usize::from(sig.text_range().start()),
DiagnosticKind::InvalidFunctionSignature,
)
})
.unwrap_or(false)
}
fn project_signature(node: &SyntaxNode) -> String {
match node.children().find(|c| c.kind() == SIGNATURE) {
Some(sig) => match first_node(&sig) {
Some(inner) => project(&inner),
None => project_flat(significant(&sig)),
},
None => project_flat(significant(node)),
}
}
fn with_error_trivia(node: &SyntaxNode, mut parts: Vec<String>) -> Vec<String> {
let s = usize::from(node.text_range().start());
if diag_at(s, DiagnosticKind::UnterminatedLiteral)
|| diag_at(s, DiagnosticKind::StringSuffixSpace)
{
if parts == ["\"\""] {
parts.clear();
}
parts.push("(error-t)".to_string());
}
parts
}
fn project_error(head: &str, node: &SyntaxNode) -> String {
let parts: Vec<String> = node
.children_with_tokens()
.filter_map(|el| match &el {
NodeOrToken::Token(t) if is_error_glyph(t.kind()) => Some("✘".to_string()),
NodeOrToken::Token(t) if is_closing_block_keyword_kind(t.kind()) => {
Some(t.text().to_string())
}
NodeOrToken::Token(t) if t.kind() == TRANSPOSE => Some(t.text().to_string()),
NodeOrToken::Token(t) if t.kind() == DOT => None,
NodeOrToken::Token(t) if is_drop_token(t.kind()) => None,
_ => project_element(&el),
})
.collect();
sexp(head, parts)
}
fn is_error_glyph(kind: SyntaxKind) -> bool {
matches!(
kind,
LPAREN | RPAREN | LBRACKET | RBRACKET | LBRACE | RBRACE | COMMA | AT
)
}
fn is_closing_block_keyword_kind(kind: SyntaxKind) -> bool {
matches!(kind, END_KW | ELSE_KW | ELSEIF_KW | CATCH_KW | FINALLY_KW)
}
fn sexp(head: &str, parts: Vec<String>) -> String {
if parts.is_empty() {
format!("({head})")
} else {
format!("({head} {})", parts.join(" "))
}
}
fn stmt_strings(node: &SyntaxNode) -> Vec<String> {
child_nodes(node).iter().map(project).collect()
}
fn project_block_child(node: &SyntaxNode) -> String {
match node.children().find(|c| c.kind() == BLOCK) {
Some(block) => {
let mut parts = stmt_strings(&block);
if parts.is_empty()
&& diag_count_from(keyword_start(node), DiagnosticKind::MissingEnd) > 0
{
parts.push("(error)".to_string());
}
sexp("block", parts)
}
None => "(block)".to_string(),
}
}
fn push_trailing_errors(node: &SyntaxNode, parts: &mut Vec<String>) {
for _ in 0..diag_count_from(keyword_start(node), DiagnosticKind::MissingEnd) {
parts.push("(error-t)".to_string());
}
}
fn project_block_child_folding_error(node: &SyntaxNode) -> String {
let Some(block) = node.children().find(|c| c.kind() == BLOCK) else {
return "(block)".to_string();
};
let mut parts = stmt_strings(&block);
for err in node.children().filter(|c| c.kind() == ERROR) {
parts.push(project(&err));
}
push_trailing_errors(node, &mut parts);
sexp("block", parts)
}
fn child_nodes(node: &SyntaxNode) -> Vec<SyntaxNode> {
node.children().collect()
}
fn project_each(nodes: Vec<SyntaxNode>) -> Vec<String> {
nodes.iter().map(project).collect()
}
fn first_node(node: &SyntaxNode) -> Option<SyntaxNode> {
node.children().next()
}
fn project_first(node: &SyntaxNode) -> String {
first_node(node).map(|n| project(&n)).unwrap_or_default()
}
fn name_text(node: &SyntaxNode) -> String {
node.children_with_tokens()
.filter_map(|el| el.into_token())
.find(|t| t.kind() == IDENT || is_keyword(t.kind()))
.map(|t| t.text().to_string())
.unwrap_or_default()
}
fn operator_token(node: &SyntaxNode) -> Option<SyntaxToken> {
node.children_with_tokens()
.filter_map(|el| el.into_token())
.find(|t| is_operator(t.kind()))
}
fn name_run_item(el: SyntaxElement) -> Option<String> {
match el {
NodeOrToken::Token(t) if t.kind() == IDENT => Some(t.text().to_string()),
NodeOrToken::Node(n) if n.kind() == NAME => Some(name_text(&n)),
NodeOrToken::Node(n) if n.kind() == INTERPOLATION => Some(project(&n)),
NodeOrToken::Node(n) if n.kind() == MACRO_NAME => Some(project_macro_name(&n)),
NodeOrToken::Token(t) if is_operator(t.kind()) => Some(t.text().to_string()),
NodeOrToken::Token(t) if is_keyword(t.kind()) => Some(t.text().to_string()),
_ => None,
}
}
fn project_flat(elems: Vec<SyntaxElement>) -> String {
match elems.as_slice() {
[one] => project_element(one).unwrap_or_default(),
[lhs, NodeOrToken::Token(op), rhs] if is_operator(op.kind()) => {
let l = project_element(lhs).unwrap_or_default();
let r = project_element(rhs).unwrap_or_default();
match infix_head(op.kind()) {
InfixHead::CallI(text) => format!("(call-i {l} {text} {r})"),
InfixHead::Special(text) => format!("({text} {l} {r})"),
InfixHead::DotCallI(text) => format!("(dotcall-i {l} {text} {r})"),
InfixHead::Dot => format!("(. {l} (quote {r}))"),
}
}
_ => elems
.iter()
.filter_map(project_element)
.collect::<Vec<_>>()
.join(" "),
}
}
fn project_element(el: &SyntaxElement) -> Option<String> {
match el {
NodeOrToken::Node(n) => Some(project(n)),
NodeOrToken::Token(t) => match t.kind() {
IDENT | INTEGER | BIN_INT | OCT_INT | HEX_INT | FLOAT | FLOAT32 => {
Some(t.text().to_string())
}
CHAR => Some(project_char(t)),
TRUE_KW => Some("true".to_string()),
FALSE_KW => Some("false".to_string()),
END_KW => Some("end".to_string()),
k if is_operator(k) => Some(t.text().to_string()),
_ => None,
},
}
}
fn significant(node: &SyntaxNode) -> Vec<SyntaxElement> {
node.children_with_tokens()
.filter(|el| match el {
NodeOrToken::Node(_) => true,
NodeOrToken::Token(t) => !is_drop_token(t.kind()),
})
.collect()
}
fn is_drop_token(kind: SyntaxKind) -> bool {
is_trivia(kind) || is_delimiter(kind) || is_keyword(kind) || kind == DOLLAR
}
fn is_trivia(kind: SyntaxKind) -> bool {
matches!(kind, WHITESPACE | NEWLINE | COMMENT | BLOCK_COMMENT)
}
fn is_delimiter(kind: SyntaxKind) -> bool {
matches!(
kind,
LPAREN | RPAREN | LBRACKET | RBRACKET | LBRACE | RBRACE | COMMA | SEMICOLON | AT
)
}
fn is_keyword(kind: SyntaxKind) -> bool {
matches!(
kind,
FUNCTION_KW
| MACRO_KW
| END_KW
| IF_KW
| ELSEIF_KW
| ELSE_KW
| BEGIN_KW
| WHILE_KW
| FOR_KW
| LET_KW
| QUOTE_KW
| TRY_KW
| CATCH_KW
| FINALLY_KW
| STRUCT_KW
| MUTABLE_KW
| MODULE_KW
| BAREMODULE_KW
| DO_KW
| RETURN_KW
| BREAK_KW
| CONTINUE_KW
| CONST_KW
| GLOBAL_KW
| LOCAL_KW
| IMPORT_KW
| USING_KW
| EXPORT_KW
| WHERE_KW
)
}