2718 lines
92 KiB
Rust
2718 lines
92 KiB
Rust
use std::collections::{HashMap, HashSet};
|
|
|
|
use crate::{
|
|
diag::Diagnostic,
|
|
sexpr::{Atom, SExpr, SExprKind},
|
|
std_runtime,
|
|
token::Span,
|
|
};
|
|
|
|
pub fn format(file: &str, source: &str, forms: &[SExpr]) -> Result<String, Vec<Diagnostic>> {
|
|
let (comments, comment_errors) = collect_comments(source)
|
|
.into_iter()
|
|
.partition::<Vec<_>, _>(|comment| comment.full_line);
|
|
let mut formatter = Formatter {
|
|
file,
|
|
comments,
|
|
next_comment: 0,
|
|
output: String::new(),
|
|
function_names: collect_function_names(forms),
|
|
struct_names: collect_struct_names(forms),
|
|
enum_names: collect_enum_names(forms),
|
|
errors: comment_errors
|
|
.into_iter()
|
|
.map(|comment| unsupported_non_full_line_comment(file, comment.span))
|
|
.collect(),
|
|
};
|
|
|
|
formatter.write_forms(forms);
|
|
|
|
if formatter.errors.is_empty() {
|
|
Ok(formatter.output)
|
|
} else {
|
|
Err(formatter.errors)
|
|
}
|
|
}
|
|
|
|
struct Formatter<'a> {
|
|
file: &'a str,
|
|
comments: Vec<LineComment>,
|
|
next_comment: usize,
|
|
output: String,
|
|
function_names: HashSet<String>,
|
|
struct_names: HashSet<String>,
|
|
enum_names: HashSet<String>,
|
|
errors: Vec<Diagnostic>,
|
|
}
|
|
|
|
impl Formatter<'_> {
|
|
fn write_forms(&mut self, forms: &[SExpr]) {
|
|
for (index, form) in forms.iter().enumerate() {
|
|
let before = self.take_comments_before(form.span.start);
|
|
|
|
if index > 0 {
|
|
self.output.push('\n');
|
|
}
|
|
|
|
self.write_comments(&before, "");
|
|
|
|
if !before.is_empty() {
|
|
self.output.push('\n');
|
|
}
|
|
|
|
match list_head(form) {
|
|
Some("module") => self.write_module(form),
|
|
Some("import") => self.write_import(form),
|
|
Some("import_c") => self.write_c_import(form),
|
|
Some("enum") => self.write_enum(form),
|
|
Some("struct") => self.write_struct(form),
|
|
Some("fn") => self.write_function(form),
|
|
Some("test") => self.write_test(form),
|
|
Some(other) => self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
format!("formatter does not support top-level form `{}`", other),
|
|
)
|
|
.with_span(form.span)
|
|
.hint("current formatter syntax is limited to module/import declarations, C imports, strict `struct` forms, strict `fn` forms, and strict top-level `test` forms"),
|
|
),
|
|
None => self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"formatter expected a top-level form",
|
|
)
|
|
.with_span(form.span),
|
|
),
|
|
}
|
|
|
|
self.output.push('\n');
|
|
}
|
|
|
|
let trailing = self.take_remaining_comments();
|
|
if !trailing.is_empty() {
|
|
if !self.output.is_empty() {
|
|
self.output.push('\n');
|
|
}
|
|
self.write_comments(&trailing, "");
|
|
}
|
|
}
|
|
|
|
fn write_module(&mut self, form: &SExpr) {
|
|
let Some(items) = expect_list(form) else {
|
|
self.errors.push(
|
|
Diagnostic::new(self.file, "InvalidModule", "module form must be a list")
|
|
.with_span(form.span),
|
|
);
|
|
return;
|
|
};
|
|
|
|
if items.len() != 2 && items.len() != 3 {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"InvalidModule",
|
|
"module form must be `(module name)` or `(module name (export ...))`",
|
|
)
|
|
.with_span(form.span),
|
|
);
|
|
return;
|
|
}
|
|
|
|
let Some(name) = expect_ident(&items[1]) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"InvalidModuleName",
|
|
"module name must be an identifier",
|
|
)
|
|
.with_span(items[1].span),
|
|
);
|
|
return;
|
|
};
|
|
|
|
self.output.push_str("(module ");
|
|
self.output.push_str(name);
|
|
if let Some(export_form) = items.get(2) {
|
|
let Some(exports) = expect_list(export_form) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"InvalidExport",
|
|
"export list must be `(export name...)`",
|
|
)
|
|
.with_span(export_form.span),
|
|
);
|
|
return;
|
|
};
|
|
if !matches!(exports.first().and_then(expect_ident), Some("export")) {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"InvalidExport",
|
|
"module option must be an export list",
|
|
)
|
|
.with_span(export_form.span),
|
|
);
|
|
return;
|
|
}
|
|
self.output.push_str(" (export");
|
|
for export in &exports[1..] {
|
|
let Some(name) = expect_ident(export) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"InvalidExport",
|
|
"exported name must be an identifier",
|
|
)
|
|
.with_span(export.span),
|
|
);
|
|
continue;
|
|
};
|
|
self.output.push(' ');
|
|
self.output.push_str(name);
|
|
}
|
|
self.output.push(')');
|
|
}
|
|
self.reject_comments_before(
|
|
form.span.end,
|
|
"formatter does not support comments inside module forms",
|
|
);
|
|
self.output.push(')');
|
|
}
|
|
|
|
fn write_import(&mut self, form: &SExpr) {
|
|
let Some(items) = expect_list(form) else {
|
|
self.errors.push(
|
|
Diagnostic::new(self.file, "InvalidImport", "import form must be a list")
|
|
.with_span(form.span),
|
|
);
|
|
return;
|
|
};
|
|
if items.len() != 3 {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"InvalidImport",
|
|
"import form must be `(import module (name...))`",
|
|
)
|
|
.with_span(form.span),
|
|
);
|
|
return;
|
|
}
|
|
let Some(module) = expect_ident(&items[1]) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"InvalidImport",
|
|
"import module must be an identifier",
|
|
)
|
|
.with_span(items[1].span),
|
|
);
|
|
return;
|
|
};
|
|
let Some(names) = expect_list(&items[2]) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"InvalidImport",
|
|
"import list must contain imported names",
|
|
)
|
|
.with_span(items[2].span),
|
|
);
|
|
return;
|
|
};
|
|
|
|
self.output.push_str("(import ");
|
|
self.output.push_str(module);
|
|
self.output.push_str(" (");
|
|
for (index, name) in names.iter().enumerate() {
|
|
let Some(name) = expect_ident(name) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"InvalidImport",
|
|
"imported name must be an identifier",
|
|
)
|
|
.with_span(name.span),
|
|
);
|
|
continue;
|
|
};
|
|
if index > 0 {
|
|
self.output.push(' ');
|
|
}
|
|
self.output.push_str(name);
|
|
}
|
|
self.output.push_str("))");
|
|
self.reject_comments_before(
|
|
form.span.end,
|
|
"formatter does not support comments inside import forms",
|
|
);
|
|
}
|
|
|
|
fn write_struct(&mut self, form: &SExpr) {
|
|
let Some(items) = expect_list(form) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"MalformedStructForm",
|
|
"struct form must be a list",
|
|
)
|
|
.with_span(form.span),
|
|
);
|
|
return;
|
|
};
|
|
|
|
if items.len() < 2 {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"MalformedStructForm",
|
|
"struct form must be `(struct Name (field type)...)`",
|
|
)
|
|
.with_span(form.span),
|
|
);
|
|
return;
|
|
}
|
|
|
|
let Some(name) = expect_ident(&items[1]) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"InvalidStructName",
|
|
"struct name must be an identifier",
|
|
)
|
|
.with_span(items[1].span),
|
|
);
|
|
return;
|
|
};
|
|
|
|
let mut fields = Vec::new();
|
|
for item in &items[2..] {
|
|
let Some(pair) = expect_list(item) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"InvalidStructField",
|
|
"struct field must be `(name type)`",
|
|
)
|
|
.with_span(item.span),
|
|
);
|
|
continue;
|
|
};
|
|
|
|
if pair.len() != 2 {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"InvalidStructField",
|
|
"struct field must be `(name type)`",
|
|
)
|
|
.with_span(item.span),
|
|
);
|
|
continue;
|
|
}
|
|
|
|
let Some(field_name) = expect_ident(&pair[0]) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"InvalidStructFieldName",
|
|
"struct field name must be an identifier",
|
|
)
|
|
.with_span(pair[0].span),
|
|
);
|
|
continue;
|
|
};
|
|
|
|
let Some(field_ty) = self.render_struct_field_type(&pair[1]) else {
|
|
continue;
|
|
};
|
|
|
|
fields.push((field_name, field_ty));
|
|
}
|
|
|
|
self.reject_comments_before(
|
|
form.span.end,
|
|
"formatter does not support comments inside struct forms",
|
|
);
|
|
|
|
self.output.push_str("(struct ");
|
|
self.output.push_str(name);
|
|
for (field, field_ty) in fields {
|
|
self.output.push('\n');
|
|
self.output.push_str(" (");
|
|
self.output.push_str(field);
|
|
self.output.push(' ');
|
|
self.output.push_str(&field_ty);
|
|
self.output.push(')');
|
|
}
|
|
self.output.push(')');
|
|
}
|
|
|
|
fn write_c_import(&mut self, form: &SExpr) {
|
|
let Some(items) = expect_list(form) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"MalformedCImport",
|
|
"`import_c` form must be a list",
|
|
)
|
|
.with_span(form.span),
|
|
);
|
|
return;
|
|
};
|
|
if items.len() != 5 {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"MalformedCImport",
|
|
"`import_c` form must be `(import_c name ((arg i32)...) -> ReturnType)`",
|
|
)
|
|
.with_span(form.span),
|
|
);
|
|
return;
|
|
}
|
|
let Some(name) = expect_ident(&items[1]) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"MalformedCImport",
|
|
"C import name must be an identifier",
|
|
)
|
|
.with_span(items[1].span),
|
|
);
|
|
return;
|
|
};
|
|
if !is_c_symbol_name(name) {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"MalformedCImport",
|
|
"C import name must be a C symbol identifier",
|
|
)
|
|
.with_span(items[1].span)
|
|
.expected("ASCII letter or `_`, followed by ASCII letters, digits, or `_`")
|
|
.found(name.to_string()),
|
|
);
|
|
return;
|
|
}
|
|
if !matches!(items[3].kind, SExprKind::Atom(Atom::Arrow)) {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"MalformedCImport",
|
|
"expected `->` in C import signature",
|
|
)
|
|
.with_span(items[3].span),
|
|
);
|
|
return;
|
|
}
|
|
let Some(params) = self.params(&items[2]) else {
|
|
return;
|
|
};
|
|
let Some(return_type) = self.render_c_import_return_type(&items[4]) else {
|
|
return;
|
|
};
|
|
|
|
self.output.push_str("(import_c ");
|
|
self.output.push_str(name);
|
|
self.output.push(' ');
|
|
self.write_params(¶ms);
|
|
self.output.push_str(" -> ");
|
|
self.output.push_str(&return_type);
|
|
self.output.push(')');
|
|
}
|
|
|
|
fn write_enum(&mut self, form: &SExpr) {
|
|
let Some(items) = expect_list(form) else {
|
|
self.errors.push(
|
|
Diagnostic::new(self.file, "MalformedEnumForm", "enum form must be a list")
|
|
.with_span(form.span),
|
|
);
|
|
return;
|
|
};
|
|
|
|
if items.len() < 2 {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"MalformedEnumForm",
|
|
"enum form must be `(enum Name Variant...)`",
|
|
)
|
|
.with_span(form.span),
|
|
);
|
|
return;
|
|
}
|
|
|
|
let Some(name) = expect_ident(&items[1]) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"InvalidEnumName",
|
|
"enum name must be an identifier",
|
|
)
|
|
.with_span(items[1].span),
|
|
);
|
|
return;
|
|
};
|
|
|
|
let mut variants = Vec::new();
|
|
let mut has_payload_variant = false;
|
|
let mut enum_payload_ty: Option<(String, Span)> = None;
|
|
for item in &items[2..] {
|
|
if let Some(variant) = expect_ident(item) {
|
|
variants.push(variant.to_string());
|
|
continue;
|
|
}
|
|
|
|
let Some(variant_items) = expect_list(item) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"InvalidEnumVariant",
|
|
"enum variants must be identifiers or unary payload forms",
|
|
)
|
|
.with_span(item.span),
|
|
);
|
|
continue;
|
|
};
|
|
if variant_items.len() != 2 {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"InvalidEnumVariant",
|
|
"enum payload variants must be unary forms",
|
|
)
|
|
.with_span(item.span)
|
|
.expected("(Variant i32), (Variant i64), (Variant f64), (Variant bool), (Variant string), or (Variant KnownStruct)"),
|
|
);
|
|
continue;
|
|
}
|
|
let Some(variant) = expect_ident(&variant_items[0]) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"InvalidEnumVariant",
|
|
"enum variant name must be an identifier",
|
|
)
|
|
.with_span(variant_items[0].span),
|
|
);
|
|
continue;
|
|
};
|
|
let payload_ty = if is_ident(&variant_items[1], "i32") {
|
|
"i32".to_string()
|
|
} else if is_ident(&variant_items[1], "i64") {
|
|
"i64".to_string()
|
|
} else if is_ident(&variant_items[1], "f64") {
|
|
"f64".to_string()
|
|
} else if is_ident(&variant_items[1], "bool") {
|
|
"bool".to_string()
|
|
} else if is_ident(&variant_items[1], "string") {
|
|
"string".to_string()
|
|
} else if let Some(name) = expect_ident(&variant_items[1]) {
|
|
if self.struct_names.contains(name) {
|
|
name.to_string()
|
|
} else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"formatter supports only unary direct i32, i64, f64, bool, string, and known non-recursive struct enum payload variants",
|
|
)
|
|
.with_span(variant_items[1].span)
|
|
.expected("i32, i64, f64, bool, string, or known non-recursive struct type"),
|
|
);
|
|
continue;
|
|
}
|
|
} else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"formatter supports only unary direct i32, i64, f64, bool, string, and known non-recursive struct enum payload variants",
|
|
)
|
|
.with_span(variant_items[1].span)
|
|
.expected("i32, i64, f64, bool, string, or known non-recursive struct type"),
|
|
);
|
|
continue;
|
|
};
|
|
if let Some((expected_ty, expected_span)) = &enum_payload_ty {
|
|
if payload_ty != *expected_ty {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"formatter requires all payload variants in one enum to share the same payload type",
|
|
)
|
|
.with_span(variant_items[1].span)
|
|
.related("first payload type in this enum", *expected_span)
|
|
.expected(expected_ty)
|
|
.found(&payload_ty)
|
|
.hint("split mixed payload kinds into separate enums"),
|
|
);
|
|
continue;
|
|
}
|
|
} else {
|
|
enum_payload_ty = Some((payload_ty.clone(), variant_items[1].span));
|
|
}
|
|
has_payload_variant = true;
|
|
variants.push(format!("({} {})", variant, payload_ty));
|
|
}
|
|
|
|
self.reject_comments_before(
|
|
form.span.end,
|
|
"formatter does not support comments inside enum forms",
|
|
);
|
|
|
|
self.output.push_str("(enum ");
|
|
self.output.push_str(name);
|
|
if has_payload_variant {
|
|
for variant in variants {
|
|
self.output.push('\n');
|
|
self.output.push_str(" ");
|
|
self.output.push_str(&variant);
|
|
}
|
|
} else {
|
|
for variant in variants {
|
|
self.output.push(' ');
|
|
self.output.push_str(&variant);
|
|
}
|
|
}
|
|
self.output.push(')');
|
|
}
|
|
|
|
fn write_function(&mut self, form: &SExpr) {
|
|
let Some(items) = expect_list(form) else {
|
|
self.errors.push(
|
|
Diagnostic::new(self.file, "InvalidFunction", "function form must be a list")
|
|
.with_span(form.span),
|
|
);
|
|
return;
|
|
};
|
|
|
|
if items.len() < 6 {
|
|
self.errors.push(
|
|
Diagnostic::new(self.file, "InvalidFunction", "function form is incomplete")
|
|
.with_span(form.span)
|
|
.hint("expected `(fn name ((arg i32) ...) -> i32 body...)`"),
|
|
);
|
|
return;
|
|
}
|
|
|
|
let Some(name) = expect_ident(&items[1]) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"InvalidFunctionName",
|
|
"function name must be an identifier",
|
|
)
|
|
.with_span(items[1].span),
|
|
);
|
|
return;
|
|
};
|
|
|
|
if !matches!(items[3].kind, SExprKind::Atom(Atom::Arrow)) {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"ExpectedArrow",
|
|
"expected `->` in function signature",
|
|
)
|
|
.with_span(items[3].span),
|
|
);
|
|
return;
|
|
}
|
|
|
|
let Some(return_type) = self.render_return_type(&items[4]) else {
|
|
return;
|
|
};
|
|
|
|
let Some(params) = self.params(&items[2]) else {
|
|
return;
|
|
};
|
|
|
|
self.reject_comments_before(
|
|
items[4].span.end,
|
|
"formatter does not support comments inside function signatures",
|
|
);
|
|
|
|
let mut env = HashSet::new();
|
|
for (param, _) in ¶ms {
|
|
env.insert((*param).to_string());
|
|
}
|
|
|
|
self.output.push_str("(fn ");
|
|
self.output.push_str(name);
|
|
self.output.push(' ');
|
|
self.write_params(¶ms);
|
|
self.output.push_str(" -> ");
|
|
self.output.push_str(&return_type);
|
|
|
|
for expr in &items[5..] {
|
|
let before = self.take_comments_before(expr.span.start);
|
|
for comment in &before {
|
|
self.output.push('\n');
|
|
self.output.push_str(" ");
|
|
self.output.push_str(&comment.text);
|
|
}
|
|
|
|
let rendered = self.render_body_expr(expr, &mut env);
|
|
self.reject_comments_before(
|
|
expr.span.end,
|
|
"formatter does not support comments inside expression forms",
|
|
);
|
|
|
|
let Some(rendered) = rendered else {
|
|
continue;
|
|
};
|
|
self.output.push('\n');
|
|
self.write_indented_rendered(" ", &rendered);
|
|
}
|
|
|
|
let trailing = self.take_comments_before(form.span.end);
|
|
if trailing.is_empty() {
|
|
self.output.push(')');
|
|
} else {
|
|
for comment in &trailing {
|
|
self.output.push('\n');
|
|
self.output.push_str(" ");
|
|
self.output.push_str(&comment.text);
|
|
}
|
|
self.output.push('\n');
|
|
self.output.push(')');
|
|
}
|
|
}
|
|
|
|
fn write_test(&mut self, form: &SExpr) {
|
|
let Some(items) = expect_list(form) else {
|
|
self.errors.push(
|
|
Diagnostic::new(self.file, "MalformedTestForm", "test form must be a list")
|
|
.with_span(form.span)
|
|
.expected(r#"(test "name" body... final-expression)"#),
|
|
);
|
|
return;
|
|
};
|
|
|
|
if items.len() < 3 {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"MalformedTestForm",
|
|
r#"test form must be `(test "name" body...)`"#,
|
|
)
|
|
.with_span(form.span)
|
|
.expected(r#"(test "name" body... final-expression)"#),
|
|
);
|
|
return;
|
|
}
|
|
|
|
let Some(name) = expect_string(&items[1]) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"MalformedTestForm",
|
|
"test name must be a string literal",
|
|
)
|
|
.with_span(items[1].span)
|
|
.expected("string literal"),
|
|
);
|
|
return;
|
|
};
|
|
|
|
if !is_valid_test_name(name) {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"InvalidTestName",
|
|
"test name must be non-empty printable ASCII without quotes, backslashes, or newlines",
|
|
)
|
|
.with_span(items[1].span),
|
|
);
|
|
return;
|
|
}
|
|
|
|
self.reject_comments_before(
|
|
items[1].span.end,
|
|
"formatter does not support comments inside test headers",
|
|
);
|
|
|
|
self.output.push_str("(test ");
|
|
self.output.push_str(&render_string_literal(name));
|
|
|
|
let mut env = HashSet::new();
|
|
for (index, expr) in items[2..].iter().enumerate() {
|
|
let before = self.take_comments_before(expr.span.start);
|
|
for comment in &before {
|
|
self.output.push('\n');
|
|
self.output.push_str(" ");
|
|
self.output.push_str(&comment.text);
|
|
}
|
|
|
|
if index + 1 < items[2..].len() && !is_sequential_test_body_form(expr) {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"MalformedTestForm",
|
|
"test body forms before the final expression must be local declarations, assignments, or while loops",
|
|
)
|
|
.with_span(expr.span)
|
|
.hint("use `(let name i32 expr)`, `(var name i32 expr)`, `(set name expr)`, or `(while condition body...)` before the final bool expression"),
|
|
);
|
|
continue;
|
|
}
|
|
|
|
let rendered = self.render_body_expr(expr, &mut env);
|
|
self.reject_comments_before(
|
|
expr.span.end,
|
|
"formatter does not support comments inside expression forms",
|
|
);
|
|
|
|
let Some(rendered) = rendered else {
|
|
continue;
|
|
};
|
|
self.output.push('\n');
|
|
self.write_indented_rendered(" ", &rendered);
|
|
}
|
|
|
|
let trailing = self.take_comments_before(form.span.end);
|
|
if trailing.is_empty() {
|
|
self.output.push(')');
|
|
} else {
|
|
for comment in &trailing {
|
|
self.output.push('\n');
|
|
self.output.push_str(" ");
|
|
self.output.push_str(&comment.text);
|
|
}
|
|
self.output.push('\n');
|
|
self.output.push(')');
|
|
}
|
|
}
|
|
|
|
fn params<'a>(&mut self, form: &'a SExpr) -> Option<Vec<(&'a str, String)>> {
|
|
let Some(items) = expect_list(form) else {
|
|
self.errors.push(
|
|
Diagnostic::new(self.file, "InvalidParams", "parameters must be a list")
|
|
.with_span(form.span),
|
|
);
|
|
return None;
|
|
};
|
|
|
|
let mut params = Vec::new();
|
|
|
|
let starting_error_count = self.errors.len();
|
|
|
|
for item in items {
|
|
let Some(pair) = expect_list(item) else {
|
|
self.errors.push(
|
|
Diagnostic::new(self.file, "InvalidParam", "parameter must be `(name i32)`")
|
|
.with_span(item.span),
|
|
);
|
|
continue;
|
|
};
|
|
|
|
if pair.len() != 2 {
|
|
self.errors.push(
|
|
Diagnostic::new(self.file, "InvalidParam", "parameter must be `(name i32)`")
|
|
.with_span(item.span),
|
|
);
|
|
continue;
|
|
}
|
|
|
|
let Some(name) = expect_ident(&pair[0]) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"InvalidParamName",
|
|
"parameter name must be an identifier",
|
|
)
|
|
.with_span(pair[0].span),
|
|
);
|
|
continue;
|
|
};
|
|
|
|
let ty = if is_ident(&pair[1], "i32") {
|
|
"i32".to_string()
|
|
} else if is_ident(&pair[1], "i64") {
|
|
"i64".to_string()
|
|
} else if is_ident(&pair[1], "u32") {
|
|
"u32".to_string()
|
|
} else if is_ident(&pair[1], "u64") {
|
|
"u64".to_string()
|
|
} else if is_ident(&pair[1], "f64") {
|
|
"f64".to_string()
|
|
} else if is_ident(&pair[1], "bool") {
|
|
"bool".to_string()
|
|
} else if is_ident(&pair[1], "string") {
|
|
"string".to_string()
|
|
} else if let Some(name) = expect_ident(&pair[1]) {
|
|
if self.struct_names.contains(name) || self.enum_names.contains(name) {
|
|
name.to_string()
|
|
} else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"formatter supports only `i32`, `i64`, `u32`, `u64`, `f64`, `bool`, `string`, known struct types, `(option i32)`, `(option i64)`, `(option u32)`, `(option u64)`, `(option f64)`, `(option bool)`, `(option string)`, `(result i32 i32)`, `(result i64 i32)`, `(result u32 i32)`, `(result u64 i32)`, `(result f64 i32)`, `(result bool i32)`, `(result string i32)`, `(array i32 N)`, `(array i64 N)`, `(array u32 N)`, `(array u64 N)`, `(array f64 N)`, `(array bool N)`, `(array string N)`, `(vec i32)`, `(vec i64)`, `(vec f64)`, `(vec bool)`, and `(vec string)` parameters",
|
|
)
|
|
.with_span(pair[1].span),
|
|
);
|
|
continue;
|
|
}
|
|
} else if let Some(items) = expect_list(&pair[1]) {
|
|
if let Some(text) = render_option_result_type(items) {
|
|
text
|
|
} else if let Some(text) =
|
|
render_supported_array_type(items, &self.struct_names, &self.enum_names)
|
|
{
|
|
text
|
|
} else if let Some(text) = render_supported_vec_type(items) {
|
|
text
|
|
} else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"formatter supports only `i32`, `i64`, `u32`, `u64`, `f64`, `bool`, `string`, known struct types, `(option i32)`, `(option i64)`, `(option u32)`, `(option u64)`, `(option f64)`, `(option bool)`, `(option string)`, `(result i32 i32)`, `(result i64 i32)`, `(result u32 i32)`, `(result u64 i32)`, `(result f64 i32)`, `(result bool i32)`, `(result string i32)`, `(array i32 N)`, `(array i64 N)`, `(array u32 N)`, `(array u64 N)`, `(array f64 N)`, `(array bool N)`, `(array string N)`, `(vec i32)`, `(vec i64)`, `(vec f64)`, `(vec bool)`, and `(vec string)` parameters",
|
|
)
|
|
.with_span(pair[1].span),
|
|
);
|
|
continue;
|
|
}
|
|
} else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"formatter supports only `i32`, `i64`, `u32`, `u64`, `f64`, `bool`, `string`, known struct types, `(option i32)`, `(option i64)`, `(option u32)`, `(option u64)`, `(option f64)`, `(option bool)`, `(option string)`, `(result i32 i32)`, `(result i64 i32)`, `(result u32 i32)`, `(result u64 i32)`, `(result f64 i32)`, `(result bool i32)`, `(result string i32)`, `(array i32 N)`, `(array i64 N)`, `(array u32 N)`, `(array u64 N)`, `(array f64 N)`, `(array bool N)`, `(array string N)`, `(vec i32)`, `(vec i64)`, `(vec f64)`, `(vec bool)`, and `(vec string)` parameters",
|
|
)
|
|
.with_span(pair[1].span),
|
|
);
|
|
continue;
|
|
};
|
|
|
|
params.push((name, ty));
|
|
}
|
|
|
|
if self.errors.len() == starting_error_count {
|
|
Some(params)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn write_params(&mut self, params: &[(&str, String)]) {
|
|
if params.is_empty() {
|
|
self.output.push_str("()");
|
|
return;
|
|
}
|
|
|
|
self.output.push('(');
|
|
for (index, (name, ty)) in params.iter().enumerate() {
|
|
if index > 0 {
|
|
self.output.push(' ');
|
|
}
|
|
self.output.push('(');
|
|
self.output.push_str(name);
|
|
self.output.push(' ');
|
|
self.output.push_str(ty);
|
|
self.output.push(')');
|
|
}
|
|
self.output.push(')');
|
|
}
|
|
|
|
fn render_return_type(&mut self, form: &SExpr) -> Option<String> {
|
|
if is_ident(form, "i32") {
|
|
return Some("i32".to_string());
|
|
}
|
|
if is_ident(form, "i64") {
|
|
return Some("i64".to_string());
|
|
}
|
|
if is_ident(form, "u32") {
|
|
return Some("u32".to_string());
|
|
}
|
|
if is_ident(form, "u64") {
|
|
return Some("u64".to_string());
|
|
}
|
|
if is_ident(form, "f64") {
|
|
return Some("f64".to_string());
|
|
}
|
|
if is_ident(form, "bool") {
|
|
return Some("bool".to_string());
|
|
}
|
|
if is_ident(form, "string") {
|
|
return Some("string".to_string());
|
|
}
|
|
|
|
if let Some(name) = expect_ident(form) {
|
|
if self.struct_names.contains(name) || self.enum_names.contains(name) {
|
|
return Some(name.to_string());
|
|
}
|
|
}
|
|
|
|
let Some(items) = expect_list(form) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"formatter supports only `i32`, `i64`, `u32`, `u64`, `f64`, `bool`, `string`, known struct types, `(option i32)`, `(option i64)`, `(option u32)`, `(option u64)`, `(option f64)`, `(option bool)`, `(option string)`, `(result i32 i32)`, `(result i64 i32)`, `(result u32 i32)`, `(result u64 i32)`, `(result f64 i32)`, `(result bool i32)`, `(result string i32)`, `(array i32 N)`, `(array i64 N)`, `(array u32 N)`, `(array u64 N)`, `(array f64 N)`, `(array bool N)`, `(array string N)`, `(vec i32)`, `(vec i64)`, `(vec f64)`, `(vec bool)`, and `(vec string)` function returns",
|
|
)
|
|
.with_span(form.span),
|
|
);
|
|
return None;
|
|
};
|
|
|
|
if let Some(text) = render_option_result_type(items) {
|
|
return Some(text);
|
|
}
|
|
|
|
if let Some(text) = render_supported_array_type(items, &self.struct_names, &self.enum_names)
|
|
{
|
|
return Some(text);
|
|
}
|
|
|
|
if let Some(text) = render_supported_vec_type(items) {
|
|
return Some(text);
|
|
}
|
|
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"formatter supports only `i32`, `i64`, `u32`, `u64`, `f64`, `bool`, `string`, known struct types, `(option i32)`, `(option i64)`, `(option u32)`, `(option u64)`, `(option f64)`, `(option bool)`, `(option string)`, `(result i32 i32)`, `(result i64 i32)`, `(result u32 i32)`, `(result u64 i32)`, `(result f64 i32)`, `(result bool i32)`, `(result string i32)`, `(array i32 N)`, `(array i64 N)`, `(array u32 N)`, `(array u64 N)`, `(array f64 N)`, `(array bool N)`, `(array string N)`, `(vec i32)`, `(vec i64)`, `(vec f64)`, `(vec bool)`, and `(vec string)` function returns",
|
|
)
|
|
.with_span(form.span),
|
|
);
|
|
None
|
|
}
|
|
|
|
fn render_c_import_return_type(&mut self, form: &SExpr) -> Option<String> {
|
|
if is_ident(form, "i32") {
|
|
return Some("i32".to_string());
|
|
}
|
|
if is_ident(form, "unit") {
|
|
return Some("unit".to_string());
|
|
}
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedCImportType",
|
|
"C import return type must be `i32` or `unit` in exp-6",
|
|
)
|
|
.with_span(form.span)
|
|
.expected("i32 or unit"),
|
|
);
|
|
None
|
|
}
|
|
|
|
fn render_body_expr(&mut self, expr: &SExpr, env: &mut HashSet<String>) -> Option<String> {
|
|
if let SExprKind::List(items) = &expr.kind {
|
|
if let Some(head) = items.first().and_then(expect_ident) {
|
|
match head {
|
|
"let" | "var" => return self.render_local(expr.span, head, items, env),
|
|
"set" => return self.render_set(expr.span, items, env),
|
|
"while" => return self.render_while(expr.span, items, env),
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
self.render_expr(expr, env)
|
|
}
|
|
|
|
fn render_expr(&mut self, expr: &SExpr, env: &mut HashSet<String>) -> Option<String> {
|
|
match &expr.kind {
|
|
SExprKind::Atom(Atom::Int(value)) => {
|
|
if i32::try_from(*value).is_err() {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"IntegerOutOfRange",
|
|
"integer literal is outside the supported i32 range",
|
|
)
|
|
.with_span(expr.span)
|
|
.expected("i32")
|
|
.found(value.to_string()),
|
|
);
|
|
None
|
|
} else {
|
|
Some(value.to_string())
|
|
}
|
|
}
|
|
SExprKind::Atom(Atom::I64(value)) => Some(format!("{}i64", value)),
|
|
SExprKind::Atom(Atom::U32(value)) => Some(format!("{}u32", value)),
|
|
SExprKind::Atom(Atom::U64(value)) => Some(format!("{}u64", value)),
|
|
SExprKind::Atom(Atom::Float(value)) => {
|
|
if !value.is_finite() {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFloatLiteral",
|
|
"f64 literals must be finite in exp-20",
|
|
)
|
|
.with_span(expr.span)
|
|
.expected("finite f64 literal")
|
|
.found(value.to_string())
|
|
.hint("NaN and infinity semantics remain deferred"),
|
|
);
|
|
None
|
|
} else {
|
|
Some(render_float_literal(*value))
|
|
}
|
|
}
|
|
SExprKind::Atom(Atom::Ident(name)) if name == "true" || name == "false" => {
|
|
Some(name.to_string())
|
|
}
|
|
SExprKind::Atom(Atom::String(value)) => Some(render_string_literal(value)),
|
|
SExprKind::Atom(Atom::Ident(name)) if env.contains(name.as_str()) => {
|
|
Some(name.to_string())
|
|
}
|
|
SExprKind::Atom(Atom::Ident(name)) => {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
format!(
|
|
"formatter does not support expression identifier `{}`",
|
|
name
|
|
),
|
|
)
|
|
.with_span(expr.span)
|
|
.hint("current formatter identifiers must be function parameters or locals"),
|
|
);
|
|
None
|
|
}
|
|
SExprKind::List(items) if items.is_empty() => {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"empty forms are unsupported",
|
|
)
|
|
.with_span(expr.span),
|
|
);
|
|
None
|
|
}
|
|
SExprKind::List(items) => {
|
|
let Some(head) = expect_ident(&items[0]) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"expression form head must be an identifier",
|
|
)
|
|
.with_span(items[0].span),
|
|
);
|
|
return None;
|
|
};
|
|
|
|
match head {
|
|
"+" | "-" | "*" | "/" | "%" | "bit_and" | "bit_or" | "bit_xor" | "=" | "<"
|
|
| ">" | "<=" | ">=" => self.render_call(expr.span, head, &items[1..], env, 2),
|
|
"and" | "or" => self.render_call(expr.span, head, &items[1..], env, 2),
|
|
"not" => self.render_call(expr.span, head, &items[1..], env, 1),
|
|
"." => self.render_field_access(expr.span, items, env),
|
|
"array" => self.render_array_constructor(expr.span, items, env),
|
|
"some" | "none" => self.render_option_constructor(expr.span, head, items, env),
|
|
"ok" | "err" => self.render_result_constructor(expr.span, head, items, env),
|
|
"is_some" | "is_none" | "is_ok" | "is_err" | "std.result.is_ok"
|
|
| "std.result.is_err" => self.render_call(expr.span, head, &items[1..], env, 1),
|
|
"unwrap_some"
|
|
| "unwrap_ok"
|
|
| "unwrap_err"
|
|
| "std.result.unwrap_ok"
|
|
| "std.result.unwrap_err" => {
|
|
self.render_call(expr.span, head, &items[1..], env, 1)
|
|
}
|
|
"index" => self.render_call(expr.span, head, &items[1..], env, 2),
|
|
"if" => self.render_if(expr.span, items, env),
|
|
"match" => self.render_match(expr.span, items, env),
|
|
"unsafe" => self.render_unsafe(expr.span, items, env),
|
|
name if std_runtime::function(name).is_some() => {
|
|
let function = std_runtime::function(name).expect("runtime function");
|
|
self.render_call(expr.span, head, &items[1..], env, function.params.len())
|
|
}
|
|
name if self.struct_names.contains(name) => {
|
|
self.render_struct_constructor(expr.span, name, &items[1..], env)
|
|
}
|
|
name if qualified_enum_name(name)
|
|
.map(|(enum_name, _)| self.enum_names.contains(enum_name))
|
|
.unwrap_or(false) =>
|
|
{
|
|
if items.len() > 2 {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"VariantConstructorArity",
|
|
"enum constructors support zero or one argument",
|
|
)
|
|
.with_span(expr.span)
|
|
.expected("0 or 1")
|
|
.found((items.len() - 1).to_string()),
|
|
);
|
|
None
|
|
} else if items.len() == 2 {
|
|
let rendered = self.render_expr(&items[1], env)?;
|
|
Some(format!("({} {})", name, rendered))
|
|
} else {
|
|
Some(format!("({})", name))
|
|
}
|
|
}
|
|
other if std_runtime::is_standard_path(other) => {
|
|
self.errors
|
|
.push(std_runtime::unsupported_standard_library_call(
|
|
self.file, expr.span, other,
|
|
));
|
|
None
|
|
}
|
|
name if self.function_names.contains(name) => {
|
|
self.render_call(expr.span, name, &items[1..], env, usize::MAX)
|
|
}
|
|
other => {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
format!("formatter does not support expression form `{}`", other),
|
|
)
|
|
.with_span(expr.span)
|
|
.hint("current formatter syntax supports integer, bool, and string literals, numeric and bitwise binary heads, boolean logic heads, comparisons, `if`, `match`, `while`, `unsafe`, arrays, structs, option/result observers and unwraps, standard-runtime calls, legacy runtime aliases, and user-defined calls"),
|
|
);
|
|
None
|
|
}
|
|
}
|
|
}
|
|
_ => {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"formatter does not support this expression",
|
|
)
|
|
.with_span(expr.span),
|
|
);
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
fn render_local(
|
|
&mut self,
|
|
span: Span,
|
|
keyword: &str,
|
|
items: &[SExpr],
|
|
env: &mut HashSet<String>,
|
|
) -> Option<String> {
|
|
if items.len() != 4 {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
format!(
|
|
"formatter expected `{}` to have name, type, and initializer",
|
|
keyword
|
|
),
|
|
)
|
|
.with_span(span),
|
|
);
|
|
return None;
|
|
}
|
|
|
|
let Some(name) = expect_ident(&items[1]) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"InvalidLocalName",
|
|
"local name must be an identifier",
|
|
)
|
|
.with_span(items[1].span),
|
|
);
|
|
return None;
|
|
};
|
|
|
|
let Some(ty) = self.render_local_type(&items[2]) else {
|
|
return None;
|
|
};
|
|
|
|
if keyword == "var" && ty.is_array {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"formatter does not support mutable array locals",
|
|
)
|
|
.with_span(items[2].span),
|
|
);
|
|
return None;
|
|
}
|
|
|
|
let initializer = self.render_expr(&items[3], env)?;
|
|
env.insert(name.to_string());
|
|
Some(format!(
|
|
"({} {} {} {})",
|
|
keyword, name, ty.text, initializer
|
|
))
|
|
}
|
|
|
|
fn render_local_type(&mut self, form: &SExpr) -> Option<RenderedType> {
|
|
if is_ident(form, "i32") {
|
|
return Some(RenderedType {
|
|
text: "i32".to_string(),
|
|
is_array: false,
|
|
});
|
|
}
|
|
|
|
if is_ident(form, "i64") {
|
|
return Some(RenderedType {
|
|
text: "i64".to_string(),
|
|
is_array: false,
|
|
});
|
|
}
|
|
|
|
if is_ident(form, "u32") {
|
|
return Some(RenderedType {
|
|
text: "u32".to_string(),
|
|
is_array: false,
|
|
});
|
|
}
|
|
|
|
if is_ident(form, "u64") {
|
|
return Some(RenderedType {
|
|
text: "u64".to_string(),
|
|
is_array: false,
|
|
});
|
|
}
|
|
|
|
if is_ident(form, "f64") {
|
|
return Some(RenderedType {
|
|
text: "f64".to_string(),
|
|
is_array: false,
|
|
});
|
|
}
|
|
|
|
if is_ident(form, "bool") {
|
|
return Some(RenderedType {
|
|
text: "bool".to_string(),
|
|
is_array: false,
|
|
});
|
|
}
|
|
|
|
if is_ident(form, "string") {
|
|
return Some(RenderedType {
|
|
text: "string".to_string(),
|
|
is_array: false,
|
|
});
|
|
}
|
|
|
|
if let Some(name) = expect_ident(form) {
|
|
if self.struct_names.contains(name) {
|
|
return Some(RenderedType {
|
|
text: name.to_string(),
|
|
is_array: false,
|
|
});
|
|
}
|
|
if self.enum_names.contains(name) {
|
|
return Some(RenderedType {
|
|
text: name.to_string(),
|
|
is_array: false,
|
|
});
|
|
}
|
|
}
|
|
|
|
let Some(items) = expect_list(form) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"formatter supports only `i32`, `i64`, `u32`, `u64`, `f64`, `bool`, `string`, known struct and enum types, `(option i32)`, `(option i64)`, `(option u32)`, `(option u64)`, `(option f64)`, `(option bool)`, `(option string)`, `(result i32 i32)`, `(result i64 i32)`, `(result u32 i32)`, `(result u64 i32)`, `(result f64 i32)`, `(result bool i32)`, `(result string i32)`, `(array i32 N)`, `(array i64 N)`, `(array u32 N)`, `(array u64 N)`, `(array f64 N)`, `(array bool N)`, `(array string N)`, `(vec i32)`, `(vec i64)`, `(vec f64)`, `(vec bool)`, and `(vec string)` locals",
|
|
)
|
|
.with_span(form.span),
|
|
);
|
|
return None;
|
|
};
|
|
|
|
if let Some(text) = render_option_result_type(items) {
|
|
return Some(RenderedType {
|
|
text,
|
|
is_array: false,
|
|
});
|
|
}
|
|
|
|
if let Some(text) = render_supported_vec_type(items) {
|
|
return Some(RenderedType {
|
|
text,
|
|
is_array: false,
|
|
});
|
|
}
|
|
|
|
if items.len() != 3 || !is_ident(&items[0], "array") {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"formatter supports only `i32`, `i64`, `u32`, `u64`, `f64`, `bool`, `string`, known struct and enum types, `(option i32)`, `(option i64)`, `(option u32)`, `(option u64)`, `(option f64)`, `(option bool)`, `(option string)`, `(result i32 i32)`, `(result i64 i32)`, `(result u32 i32)`, `(result u64 i32)`, `(result f64 i32)`, `(result bool i32)`, `(result string i32)`, `(array i32 N)`, `(array i64 N)`, `(array u32 N)`, `(array u64 N)`, `(array f64 N)`, `(array bool N)`, `(array string N)`, `(vec i32)`, `(vec i64)`, `(vec f64)`, `(vec bool)`, and `(vec string)` locals",
|
|
)
|
|
.with_span(form.span),
|
|
);
|
|
return None;
|
|
}
|
|
|
|
let Some(elem_ty) = render_supported_array_constructor_type(
|
|
&items[1],
|
|
&self.struct_names,
|
|
&self.enum_names,
|
|
) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"formatter supports only `i32`, `i64`, `u32`, `u64`, `f64`, `bool`, `string`, known struct and enum types, `(option i32)`, `(option i64)`, `(option u32)`, `(option u64)`, `(option f64)`, `(option bool)`, `(option string)`, `(result i32 i32)`, `(result i64 i32)`, `(result u32 i32)`, `(result u64 i32)`, `(result f64 i32)`, `(result bool i32)`, `(result string i32)`, `(array i32 N)`, `(array i64 N)`, `(array u32 N)`, `(array u64 N)`, `(array f64 N)`, `(array bool N)`, `(array string N)`, `(vec i32)`, `(vec i64)`, `(vec f64)`, `(vec bool)`, and `(vec string)` locals",
|
|
)
|
|
.with_span(form.span),
|
|
);
|
|
return None;
|
|
};
|
|
|
|
let Some(len) = expect_int(&items[2]) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"formatter expected array local length to be an integer literal",
|
|
)
|
|
.with_span(items[2].span),
|
|
);
|
|
return None;
|
|
};
|
|
|
|
if len <= 0 {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"formatter supports only positive-length array locals",
|
|
)
|
|
.with_span(items[2].span),
|
|
);
|
|
return None;
|
|
}
|
|
|
|
Some(RenderedType {
|
|
text: format!("(array {} {})", elem_ty, len),
|
|
is_array: true,
|
|
})
|
|
}
|
|
|
|
fn render_struct_field_type(&mut self, form: &SExpr) -> Option<String> {
|
|
if is_ident(form, "i32") {
|
|
return Some("i32".to_string());
|
|
}
|
|
if is_ident(form, "i64") {
|
|
return Some("i64".to_string());
|
|
}
|
|
if is_ident(form, "f64") {
|
|
return Some("f64".to_string());
|
|
}
|
|
if is_ident(form, "bool") {
|
|
return Some("bool".to_string());
|
|
}
|
|
if is_ident(form, "string") {
|
|
return Some("string".to_string());
|
|
}
|
|
|
|
if let Some(name) = expect_ident(form) {
|
|
if self.struct_names.contains(name) || self.enum_names.contains(name) {
|
|
return Some(name.to_string());
|
|
}
|
|
}
|
|
|
|
let Some(items) = expect_list(form) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"formatter supports direct scalar/string/enum fields, direct fixed-array fields over current released element families, current concrete vec/option/result fields, and current non-recursive struct fields",
|
|
)
|
|
.with_span(form.span),
|
|
);
|
|
return None;
|
|
};
|
|
|
|
if let Some(text) = render_option_result_type(items) {
|
|
return Some(text);
|
|
}
|
|
|
|
if let Some(text) = render_supported_array_type(items, &self.struct_names, &self.enum_names)
|
|
{
|
|
return Some(text);
|
|
}
|
|
|
|
if let Some(text) = render_supported_vec_type(items) {
|
|
return Some(text);
|
|
}
|
|
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"formatter supports direct scalar/string/enum fields, direct fixed-array fields over current released element families, current concrete vec/option/result fields, and current non-recursive struct fields",
|
|
)
|
|
.with_span(form.span),
|
|
);
|
|
None
|
|
}
|
|
|
|
fn render_set(
|
|
&mut self,
|
|
span: Span,
|
|
items: &[SExpr],
|
|
env: &mut HashSet<String>,
|
|
) -> Option<String> {
|
|
if items.len() != 3 {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"formatter expected `set` to have a name and value",
|
|
)
|
|
.with_span(span),
|
|
);
|
|
return None;
|
|
}
|
|
|
|
let Some(name) = expect_ident(&items[1]) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"InvalidSetTarget",
|
|
"`set` target must be an identifier",
|
|
)
|
|
.with_span(items[1].span),
|
|
);
|
|
return None;
|
|
};
|
|
|
|
if !env.contains(name) {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
format!("formatter does not know local `{}`", name),
|
|
)
|
|
.with_span(items[1].span),
|
|
);
|
|
return None;
|
|
}
|
|
|
|
let value = self.render_expr(&items[2], env)?;
|
|
Some(format!("(set {} {})", name, value))
|
|
}
|
|
|
|
fn render_while(
|
|
&mut self,
|
|
span: Span,
|
|
items: &[SExpr],
|
|
env: &mut HashSet<String>,
|
|
) -> Option<String> {
|
|
if items.len() < 3 {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"formatter expected `while` to have a condition and body",
|
|
)
|
|
.with_span(span),
|
|
);
|
|
return None;
|
|
}
|
|
|
|
let condition = self.render_expr(&items[1], env)?;
|
|
let mut output = format!("(while {}", condition);
|
|
|
|
for item in &items[2..] {
|
|
if is_local_body_form(item) {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"formatter does not support local declarations inside `while` bodies",
|
|
)
|
|
.with_span(item.span)
|
|
.hint("declare loop locals before the `while` form"),
|
|
);
|
|
return None;
|
|
}
|
|
|
|
if matches!(list_head(item), Some("while")) {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"formatter does not support nested `while` loops",
|
|
)
|
|
.with_span(item.span)
|
|
.hint("keep first-pass loop bodies flat"),
|
|
);
|
|
return None;
|
|
}
|
|
|
|
let rendered = self.render_body_expr(item, env)?;
|
|
output.push('\n');
|
|
push_indented(&mut output, " ", &rendered);
|
|
}
|
|
|
|
output.push(')');
|
|
Some(output)
|
|
}
|
|
|
|
fn render_if(
|
|
&mut self,
|
|
span: Span,
|
|
items: &[SExpr],
|
|
env: &mut HashSet<String>,
|
|
) -> Option<String> {
|
|
if items.len() != 4 {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"formatter expected `if` to have condition, then expression, and else expression",
|
|
)
|
|
.with_span(span),
|
|
);
|
|
return None;
|
|
}
|
|
|
|
let condition = self.render_expr(&items[1], env)?;
|
|
let then_expr = self.render_expr(&items[2], env)?;
|
|
let else_expr = self.render_expr(&items[3], env)?;
|
|
let mut output = format!("(if {}", condition);
|
|
output.push('\n');
|
|
push_indented(&mut output, " ", &then_expr);
|
|
output.push('\n');
|
|
push_indented(&mut output, " ", &else_expr);
|
|
output.push(')');
|
|
Some(output)
|
|
}
|
|
|
|
fn render_match(
|
|
&mut self,
|
|
span: Span,
|
|
items: &[SExpr],
|
|
env: &mut HashSet<String>,
|
|
) -> Option<String> {
|
|
if items.len() < 2 {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"MalformedMatchPattern",
|
|
"`match` expects a subject",
|
|
)
|
|
.with_span(span)
|
|
.expected("(match subject (pattern body...) (pattern body...))"),
|
|
);
|
|
return None;
|
|
}
|
|
|
|
let subject = self.render_expr(&items[1], env)?;
|
|
let mut output = format!("(match {}", subject);
|
|
let mut seen = HashMap::new();
|
|
|
|
for arm in &items[2..] {
|
|
let Some(arm_items) = expect_list(arm) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"MalformedMatchPattern",
|
|
"match arm must be a list containing a pattern and body",
|
|
)
|
|
.with_span(arm.span),
|
|
);
|
|
return None;
|
|
};
|
|
|
|
if arm_items.len() < 2 {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"MalformedMatchPattern",
|
|
"match arm must contain a pattern and at least one body expression",
|
|
)
|
|
.with_span(arm.span),
|
|
);
|
|
return None;
|
|
}
|
|
|
|
let pattern = self.render_match_pattern(&arm_items[0])?;
|
|
if let Some(original) = seen.insert(pattern.kind, arm_items[0].span) {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"DuplicateMatchArm",
|
|
format!("duplicate `{}` match arm", pattern.kind),
|
|
)
|
|
.with_span(arm_items[0].span)
|
|
.related("original match arm", original),
|
|
);
|
|
return None;
|
|
}
|
|
|
|
let mut arm_env = env.clone();
|
|
if let Some(binding) = pattern.binding {
|
|
arm_env.insert(binding.to_string());
|
|
}
|
|
|
|
let before_arm = self.take_comments_before(arm.span.start);
|
|
for comment in &before_arm {
|
|
output.push('\n');
|
|
output.push_str(" ");
|
|
output.push_str(&comment.text);
|
|
}
|
|
|
|
output.push('\n');
|
|
output.push_str(" (");
|
|
output.push_str(&pattern.text);
|
|
|
|
for body_expr in &arm_items[1..] {
|
|
let before_expr = self.take_comments_before(body_expr.span.start);
|
|
for comment in &before_expr {
|
|
output.push('\n');
|
|
output.push_str(" ");
|
|
output.push_str(&comment.text);
|
|
}
|
|
|
|
let rendered = self.render_body_expr(body_expr, &mut arm_env)?;
|
|
self.reject_comments_before(
|
|
body_expr.span.end,
|
|
"formatter does not support comments inside expression forms",
|
|
);
|
|
|
|
output.push('\n');
|
|
push_indented(&mut output, " ", &rendered);
|
|
}
|
|
|
|
let trailing = self.take_comments_before(arm.span.end);
|
|
for comment in &trailing {
|
|
output.push('\n');
|
|
output.push_str(" ");
|
|
output.push_str(&comment.text);
|
|
}
|
|
|
|
output.push(')');
|
|
}
|
|
|
|
let has_some = seen.contains_key("some");
|
|
let has_none = seen.contains_key("none");
|
|
let has_ok = seen.contains_key("ok");
|
|
let has_err = seen.contains_key("err");
|
|
let option_family = has_some || has_none;
|
|
let result_family = has_ok || has_err;
|
|
|
|
if option_family && result_family {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"MalformedMatchPattern",
|
|
"match arms must belong to one pattern family",
|
|
)
|
|
.with_span(span),
|
|
);
|
|
return None;
|
|
}
|
|
|
|
if option_family && (!has_some || !has_none) {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"NonExhaustiveMatch",
|
|
"option match must contain `some` and `none` arms",
|
|
)
|
|
.with_span(span)
|
|
.expected("some and none"),
|
|
);
|
|
return None;
|
|
}
|
|
|
|
if result_family && (!has_ok || !has_err) {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"NonExhaustiveMatch",
|
|
"result match must contain `ok` and `err` arms",
|
|
)
|
|
.with_span(span)
|
|
.expected("ok and err"),
|
|
);
|
|
return None;
|
|
}
|
|
|
|
let trailing = self.take_comments_before(span.end);
|
|
for comment in &trailing {
|
|
output.push('\n');
|
|
output.push_str(" ");
|
|
output.push_str(&comment.text);
|
|
}
|
|
|
|
output.push(')');
|
|
Some(output)
|
|
}
|
|
|
|
fn render_match_pattern<'a>(&mut self, form: &'a SExpr) -> Option<RenderedMatchPattern<'a>> {
|
|
let Some(items) = expect_list(form) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"MalformedMatchPattern",
|
|
"match arm pattern must be a list",
|
|
)
|
|
.with_span(form.span)
|
|
.expected("(some binding), (none), (ok binding), or (err binding)"),
|
|
);
|
|
return None;
|
|
};
|
|
|
|
let Some(kind) = items.first().and_then(expect_ident) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"MalformedMatchPattern",
|
|
"match arm pattern head must be an identifier",
|
|
)
|
|
.with_span(form.span),
|
|
);
|
|
return None;
|
|
};
|
|
|
|
match kind {
|
|
"some" | "ok" | "err" => {
|
|
if items.len() != 2 {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"MalformedMatchPattern",
|
|
format!("`{}` match pattern requires one payload binding", kind),
|
|
)
|
|
.with_span(form.span),
|
|
);
|
|
return None;
|
|
}
|
|
|
|
let Some(binding) = expect_ident(&items[1]) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"MalformedMatchPattern",
|
|
"match payload binding must be an identifier",
|
|
)
|
|
.with_span(items[1].span),
|
|
);
|
|
return None;
|
|
};
|
|
|
|
Some(RenderedMatchPattern {
|
|
kind,
|
|
binding: Some(binding),
|
|
text: format!("({} {})", kind, binding),
|
|
})
|
|
}
|
|
"none" => {
|
|
if items.len() != 1 {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"MalformedMatchPattern",
|
|
"`none` match pattern does not take a payload binding",
|
|
)
|
|
.with_span(form.span),
|
|
);
|
|
return None;
|
|
}
|
|
|
|
Some(RenderedMatchPattern {
|
|
kind,
|
|
binding: None,
|
|
text: "(none)".to_string(),
|
|
})
|
|
}
|
|
name if qualified_enum_name(name)
|
|
.map(|(enum_name, _)| self.enum_names.contains(enum_name))
|
|
.unwrap_or(false) =>
|
|
{
|
|
if items.len() > 2 {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"InvalidEnumMatchArm",
|
|
"enum match patterns support at most one payload binding",
|
|
)
|
|
.with_span(form.span)
|
|
.expected("(Name.Variant) or (Name.Variant binding)"),
|
|
);
|
|
return None;
|
|
}
|
|
|
|
let binding = if items.len() == 2 {
|
|
let Some(binding) = expect_ident(&items[1]) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"MalformedMatchPattern",
|
|
"match payload binding must be an identifier",
|
|
)
|
|
.with_span(items[1].span),
|
|
);
|
|
return None;
|
|
};
|
|
Some(binding)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
Some(RenderedMatchPattern {
|
|
kind: name,
|
|
binding,
|
|
text: binding
|
|
.map(|binding| format!("({} {})", name, binding))
|
|
.unwrap_or_else(|| format!("({})", name)),
|
|
})
|
|
}
|
|
_ => {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"MalformedMatchPattern",
|
|
format!("unsupported match pattern `{}`", kind),
|
|
)
|
|
.with_span(form.span),
|
|
);
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
fn render_unsafe(
|
|
&mut self,
|
|
span: Span,
|
|
items: &[SExpr],
|
|
env: &mut HashSet<String>,
|
|
) -> Option<String> {
|
|
if items.len() < 2 {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"MalformedUnsafeForm",
|
|
"`unsafe` block must contain a final expression",
|
|
)
|
|
.with_span(span)
|
|
.expected("(unsafe body-form... final-expression)"),
|
|
);
|
|
return None;
|
|
}
|
|
|
|
let mut scoped_env = env.clone();
|
|
let mut output = "(unsafe".to_string();
|
|
|
|
for item in &items[1..] {
|
|
let rendered = self.render_body_expr(item, &mut scoped_env)?;
|
|
output.push('\n');
|
|
push_indented(&mut output, " ", &rendered);
|
|
}
|
|
|
|
output.push(')');
|
|
Some(output)
|
|
}
|
|
|
|
fn render_field_access(
|
|
&mut self,
|
|
span: Span,
|
|
items: &[SExpr],
|
|
env: &mut HashSet<String>,
|
|
) -> Option<String> {
|
|
if items.len() != 3 {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"formatter expected field access to be `(. value field)`",
|
|
)
|
|
.with_span(span),
|
|
);
|
|
return None;
|
|
}
|
|
|
|
let value = self.render_expr(&items[1], env)?;
|
|
let Some(field) = expect_ident(&items[2]) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"InvalidFieldName",
|
|
"field name must be an identifier",
|
|
)
|
|
.with_span(items[2].span),
|
|
);
|
|
return None;
|
|
};
|
|
|
|
Some(format!("(. {} {})", value, field))
|
|
}
|
|
|
|
fn render_struct_constructor(
|
|
&mut self,
|
|
span: Span,
|
|
name: &str,
|
|
fields: &[SExpr],
|
|
env: &mut HashSet<String>,
|
|
) -> Option<String> {
|
|
let mut output = String::new();
|
|
output.push('(');
|
|
output.push_str(name);
|
|
|
|
for field in fields {
|
|
let Some(pair) = expect_list(field) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"formatter expected struct constructor fields to be `(field value)`",
|
|
)
|
|
.with_span(field.span),
|
|
);
|
|
return None;
|
|
};
|
|
|
|
if pair.len() != 2 {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"formatter expected struct constructor fields to be `(field value)`",
|
|
)
|
|
.with_span(field.span),
|
|
);
|
|
return None;
|
|
}
|
|
|
|
let Some(field_name) = expect_ident(&pair[0]) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"InvalidStructConstructorField",
|
|
"struct constructor field name must be an identifier",
|
|
)
|
|
.with_span(pair[0].span),
|
|
);
|
|
return None;
|
|
};
|
|
|
|
let value = self.render_expr(&pair[1], env)?;
|
|
output.push_str(" (");
|
|
output.push_str(field_name);
|
|
output.push(' ');
|
|
output.push_str(&value);
|
|
output.push(')');
|
|
}
|
|
|
|
if fields.is_empty() {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"formatter expected at least one struct constructor field",
|
|
)
|
|
.with_span(span),
|
|
);
|
|
return None;
|
|
}
|
|
|
|
output.push(')');
|
|
Some(output)
|
|
}
|
|
|
|
fn render_array_constructor(
|
|
&mut self,
|
|
span: Span,
|
|
items: &[SExpr],
|
|
env: &mut HashSet<String>,
|
|
) -> Option<String> {
|
|
if items.len() < 2 {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"formatter expected array constructor to be `(array TYPE value...)`",
|
|
)
|
|
.with_span(span),
|
|
);
|
|
return None;
|
|
}
|
|
|
|
let Some(elem_ty) = render_supported_array_constructor_type(
|
|
&items[1],
|
|
&self.struct_names,
|
|
&self.enum_names,
|
|
) else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"formatter supports only direct scalar `i32`, `i64`, `f64`, `bool`, or `string` array constructors",
|
|
)
|
|
.with_span(items[1].span),
|
|
);
|
|
return None;
|
|
};
|
|
|
|
if items.len() == 2 {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"formatter expected at least one array element",
|
|
)
|
|
.with_span(span),
|
|
);
|
|
return None;
|
|
}
|
|
|
|
let mut output = format!("(array {}", elem_ty);
|
|
for item in &items[2..] {
|
|
let rendered = self.render_expr(item, env)?;
|
|
output.push(' ');
|
|
output.push_str(&rendered);
|
|
}
|
|
output.push(')');
|
|
Some(output)
|
|
}
|
|
|
|
fn render_option_constructor(
|
|
&mut self,
|
|
span: Span,
|
|
name: &str,
|
|
items: &[SExpr],
|
|
env: &mut HashSet<String>,
|
|
) -> Option<String> {
|
|
let expected_len = if name == "some" { 3 } else { 2 };
|
|
if items.len() != expected_len {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
format!(
|
|
"formatter expected `{}` to be `{}`",
|
|
name,
|
|
if name == "some" {
|
|
"(some i32 value), (some i64 value), (some u32 value), (some u64 value), (some f64 value), (some bool value), or (some string value)"
|
|
} else {
|
|
"(none i32), (none i64), (none u32), (none u64), (none f64), (none bool), or (none string)"
|
|
}
|
|
),
|
|
)
|
|
.with_span(span),
|
|
);
|
|
return None;
|
|
}
|
|
|
|
let payload_type = if is_ident(&items[1], "i32") {
|
|
"i32"
|
|
} else if is_ident(&items[1], "i64") {
|
|
"i64"
|
|
} else if is_ident(&items[1], "u32") {
|
|
"u32"
|
|
} else if is_ident(&items[1], "u64") {
|
|
"u64"
|
|
} else if is_ident(&items[1], "f64") {
|
|
"f64"
|
|
} else if is_ident(&items[1], "bool") {
|
|
"bool"
|
|
} else if is_ident(&items[1], "string") {
|
|
"string"
|
|
} else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"formatter supports only `i32`, `i64`, `u32`, `u64`, `f64`, `bool`, and `string` option payloads",
|
|
)
|
|
.with_span(items[1].span),
|
|
);
|
|
return None;
|
|
};
|
|
|
|
if name == "none" {
|
|
return Some(format!("(none {})", payload_type));
|
|
}
|
|
|
|
let value = self.render_expr(&items[2], env)?;
|
|
Some(format!("(some {} {})", payload_type, value))
|
|
}
|
|
|
|
fn render_result_constructor(
|
|
&mut self,
|
|
span: Span,
|
|
name: &str,
|
|
items: &[SExpr],
|
|
env: &mut HashSet<String>,
|
|
) -> Option<String> {
|
|
if items.len() != 4 {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
format!(
|
|
"formatter expected `{}` to be `({} i32 i32 value)`, `({} i64 i32 value)`, `({} u32 i32 value)`, `({} u64 i32 value)`, `({} f64 i32 value)`, `({} bool i32 value)`, or `({} string i32 value)`",
|
|
name, name, name, name, name, name, name, name
|
|
),
|
|
)
|
|
.with_span(span),
|
|
);
|
|
return None;
|
|
}
|
|
|
|
let result_type = if is_ident(&items[1], "i32") && is_ident(&items[2], "i32") {
|
|
"i32 i32"
|
|
} else if is_ident(&items[1], "i64") && is_ident(&items[2], "i32") {
|
|
"i64 i32"
|
|
} else if is_ident(&items[1], "u32") && is_ident(&items[2], "i32") {
|
|
"u32 i32"
|
|
} else if is_ident(&items[1], "u64") && is_ident(&items[2], "i32") {
|
|
"u64 i32"
|
|
} else if is_ident(&items[1], "f64") && is_ident(&items[2], "i32") {
|
|
"f64 i32"
|
|
} else if is_ident(&items[1], "bool") && is_ident(&items[2], "i32") {
|
|
"bool i32"
|
|
} else if is_ident(&items[1], "string") && is_ident(&items[2], "i32") {
|
|
"string i32"
|
|
} else {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
"formatter supports only `(result i32 i32)`, `(result i64 i32)`, `(result u32 i32)`, `(result u64 i32)`, `(result f64 i32)`, `(result bool i32)`, and `(result string i32)` constructors",
|
|
)
|
|
.with_span(span),
|
|
);
|
|
return None;
|
|
};
|
|
|
|
let value = self.render_expr(&items[3], env)?;
|
|
Some(format!("({} {} {})", name, result_type, value))
|
|
}
|
|
|
|
fn render_call(
|
|
&mut self,
|
|
span: Span,
|
|
name: &str,
|
|
args: &[SExpr],
|
|
env: &mut HashSet<String>,
|
|
exact_arity: usize,
|
|
) -> Option<String> {
|
|
if exact_arity != usize::MAX && args.len() != exact_arity {
|
|
self.errors.push(
|
|
Diagnostic::new(
|
|
self.file,
|
|
"UnsupportedFormatterForm",
|
|
format!(
|
|
"formatter expected `{}` to have {} argument(s)",
|
|
name, exact_arity
|
|
),
|
|
)
|
|
.with_span(span),
|
|
);
|
|
return None;
|
|
}
|
|
|
|
let mut output = String::new();
|
|
output.push('(');
|
|
output.push_str(name);
|
|
for arg in args {
|
|
let rendered = self.render_expr(arg, env)?;
|
|
output.push(' ');
|
|
output.push_str(&rendered);
|
|
}
|
|
output.push(')');
|
|
Some(output)
|
|
}
|
|
|
|
fn write_indented_rendered(&mut self, indent: &str, rendered: &str) {
|
|
push_indented(&mut self.output, indent, rendered);
|
|
}
|
|
|
|
fn take_comments_before(&mut self, offset: usize) -> Vec<LineComment> {
|
|
let start = self.next_comment;
|
|
while self
|
|
.comments
|
|
.get(self.next_comment)
|
|
.is_some_and(|comment| comment.start < offset)
|
|
{
|
|
self.next_comment += 1;
|
|
}
|
|
self.comments[start..self.next_comment].to_vec()
|
|
}
|
|
|
|
fn take_remaining_comments(&mut self) -> Vec<LineComment> {
|
|
let comments = self.comments[self.next_comment..].to_vec();
|
|
self.next_comment = self.comments.len();
|
|
comments
|
|
}
|
|
|
|
fn write_comments(&mut self, comments: &[LineComment], indent: &str) {
|
|
for comment in comments {
|
|
self.output.push_str(indent);
|
|
self.output.push_str(&comment.text);
|
|
self.output.push('\n');
|
|
}
|
|
}
|
|
|
|
fn reject_comments_before(&mut self, offset: usize, message: &'static str) {
|
|
let comments = self.take_comments_before(offset);
|
|
for comment in comments {
|
|
self.errors.push(
|
|
Diagnostic::new(self.file, "UnsupportedFormatterComment", message)
|
|
.with_span(comment.span)
|
|
.hint(
|
|
"move the full-line comment between body expressions or outside the form",
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
struct LineComment {
|
|
start: usize,
|
|
span: Span,
|
|
text: String,
|
|
full_line: bool,
|
|
}
|
|
|
|
struct RenderedType {
|
|
text: String,
|
|
is_array: bool,
|
|
}
|
|
|
|
struct RenderedMatchPattern<'a> {
|
|
kind: &'a str,
|
|
binding: Option<&'a str>,
|
|
text: String,
|
|
}
|
|
|
|
fn collect_function_names(forms: &[SExpr]) -> HashSet<String> {
|
|
let mut names = HashSet::new();
|
|
for form in forms {
|
|
let Some(items) = expect_list(form) else {
|
|
continue;
|
|
};
|
|
match items.first() {
|
|
Some(head) if is_ident(head, "fn") => {
|
|
if let Some(name) = items.get(1).and_then(expect_ident) {
|
|
names.insert(name.to_string());
|
|
}
|
|
}
|
|
Some(head) if is_ident(head, "import_c") => {
|
|
if let Some(name) = items.get(1).and_then(expect_ident) {
|
|
names.insert(name.to_string());
|
|
}
|
|
}
|
|
Some(head) if is_ident(head, "import") => {
|
|
if let Some(imports) = items.get(2).and_then(expect_list) {
|
|
for import in imports {
|
|
if let Some(name) = expect_ident(import) {
|
|
names.insert(name.to_string());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
names
|
|
}
|
|
|
|
fn collect_struct_names(forms: &[SExpr]) -> HashSet<String> {
|
|
forms
|
|
.iter()
|
|
.filter_map(|form| {
|
|
let items = expect_list(form)?;
|
|
if !is_ident(items.first()?, "struct") {
|
|
return None;
|
|
}
|
|
expect_ident(items.get(1)?).map(str::to_string)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn collect_enum_names(forms: &[SExpr]) -> HashSet<String> {
|
|
forms
|
|
.iter()
|
|
.filter_map(|form| {
|
|
let items = expect_list(form)?;
|
|
if !is_ident(items.first()?, "enum") {
|
|
return None;
|
|
}
|
|
expect_ident(items.get(1)?).map(str::to_string)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn qualified_enum_name(name: &str) -> Option<(&str, &str)> {
|
|
let (enum_name, variant) = name.split_once('.')?;
|
|
if enum_name.is_empty() || variant.is_empty() || variant.contains('.') {
|
|
return None;
|
|
}
|
|
Some((enum_name, variant))
|
|
}
|
|
|
|
fn collect_comments(source: &str) -> Vec<LineComment> {
|
|
let mut comments = Vec::new();
|
|
let bytes = source.as_bytes();
|
|
let mut line_start = 0;
|
|
let mut i = 0;
|
|
let mut in_string = false;
|
|
let mut escaped = false;
|
|
|
|
while i < bytes.len() {
|
|
let byte = bytes[i];
|
|
|
|
if in_string {
|
|
if escaped {
|
|
escaped = false;
|
|
i += 1;
|
|
continue;
|
|
}
|
|
|
|
match byte {
|
|
b'\\' => {
|
|
escaped = true;
|
|
i += 1;
|
|
}
|
|
b'"' => {
|
|
in_string = false;
|
|
i += 1;
|
|
}
|
|
b'\n' => {
|
|
line_start = i + 1;
|
|
i += 1;
|
|
}
|
|
_ => i += 1,
|
|
}
|
|
continue;
|
|
}
|
|
|
|
match byte {
|
|
b'"' => {
|
|
in_string = true;
|
|
i += 1;
|
|
}
|
|
b'\n' => {
|
|
line_start = i + 1;
|
|
i += 1;
|
|
}
|
|
b';' => {
|
|
let comment_start = i;
|
|
let mut comment_end = i;
|
|
while comment_end < bytes.len() && bytes[comment_end] != b'\n' {
|
|
comment_end += 1;
|
|
}
|
|
|
|
let trimmed_end = trim_ascii_horizontal_end(source, comment_start, comment_end);
|
|
let full_line = source[line_start..comment_start]
|
|
.bytes()
|
|
.all(|prefix| matches!(prefix, b' ' | b'\t'));
|
|
let text = if full_line {
|
|
source[comment_start..trimmed_end].to_string()
|
|
} else {
|
|
String::new()
|
|
};
|
|
|
|
comments.push(LineComment {
|
|
start: comment_start,
|
|
span: Span::new(comment_start, trimmed_end),
|
|
text,
|
|
full_line,
|
|
});
|
|
|
|
i = comment_end;
|
|
}
|
|
_ => i += 1,
|
|
}
|
|
}
|
|
|
|
comments
|
|
}
|
|
|
|
fn trim_ascii_horizontal_end(source: &str, start: usize, mut end: usize) -> usize {
|
|
let bytes = source.as_bytes();
|
|
while end > start && matches!(bytes[end - 1], b' ' | b'\t') {
|
|
end -= 1;
|
|
}
|
|
end
|
|
}
|
|
|
|
fn unsupported_non_full_line_comment(file: &str, span: Span) -> Diagnostic {
|
|
Diagnostic::new(
|
|
file,
|
|
"UnsupportedFormatterComment",
|
|
"formatter supports comments only as full-line comments in stable positions",
|
|
)
|
|
.with_span(span)
|
|
.hint(
|
|
"move the comment to its own line before a top-level form, between body expressions, or after the final top-level form",
|
|
)
|
|
}
|
|
|
|
fn list_head(form: &SExpr) -> Option<&str> {
|
|
let items = expect_list(form)?;
|
|
let first = items.first()?;
|
|
expect_ident(first)
|
|
}
|
|
|
|
fn render_option_result_type(items: &[SExpr]) -> Option<String> {
|
|
if items.len() == 2 && is_ident(&items[0], "option") && is_ident(&items[1], "i32") {
|
|
return Some("(option i32)".to_string());
|
|
}
|
|
|
|
if items.len() == 2 && is_ident(&items[0], "option") && is_ident(&items[1], "i64") {
|
|
return Some("(option i64)".to_string());
|
|
}
|
|
|
|
if items.len() == 2 && is_ident(&items[0], "option") && is_ident(&items[1], "u32") {
|
|
return Some("(option u32)".to_string());
|
|
}
|
|
|
|
if items.len() == 2 && is_ident(&items[0], "option") && is_ident(&items[1], "u64") {
|
|
return Some("(option u64)".to_string());
|
|
}
|
|
|
|
if items.len() == 2 && is_ident(&items[0], "option") && is_ident(&items[1], "f64") {
|
|
return Some("(option f64)".to_string());
|
|
}
|
|
|
|
if items.len() == 2 && is_ident(&items[0], "option") && is_ident(&items[1], "bool") {
|
|
return Some("(option bool)".to_string());
|
|
}
|
|
|
|
if items.len() == 2 && is_ident(&items[0], "option") && is_ident(&items[1], "string") {
|
|
return Some("(option string)".to_string());
|
|
}
|
|
|
|
if items.len() == 3
|
|
&& is_ident(&items[0], "result")
|
|
&& is_ident(&items[1], "i32")
|
|
&& is_ident(&items[2], "i32")
|
|
{
|
|
return Some("(result i32 i32)".to_string());
|
|
}
|
|
|
|
if items.len() == 3
|
|
&& is_ident(&items[0], "result")
|
|
&& is_ident(&items[1], "i64")
|
|
&& is_ident(&items[2], "i32")
|
|
{
|
|
return Some("(result i64 i32)".to_string());
|
|
}
|
|
|
|
if items.len() == 3
|
|
&& is_ident(&items[0], "result")
|
|
&& is_ident(&items[1], "u32")
|
|
&& is_ident(&items[2], "i32")
|
|
{
|
|
return Some("(result u32 i32)".to_string());
|
|
}
|
|
|
|
if items.len() == 3
|
|
&& is_ident(&items[0], "result")
|
|
&& is_ident(&items[1], "u64")
|
|
&& is_ident(&items[2], "i32")
|
|
{
|
|
return Some("(result u64 i32)".to_string());
|
|
}
|
|
|
|
if items.len() == 3
|
|
&& is_ident(&items[0], "result")
|
|
&& is_ident(&items[1], "f64")
|
|
&& is_ident(&items[2], "i32")
|
|
{
|
|
return Some("(result f64 i32)".to_string());
|
|
}
|
|
|
|
if items.len() == 3
|
|
&& is_ident(&items[0], "result")
|
|
&& is_ident(&items[1], "bool")
|
|
&& is_ident(&items[2], "i32")
|
|
{
|
|
return Some("(result bool i32)".to_string());
|
|
}
|
|
|
|
if items.len() == 3
|
|
&& is_ident(&items[0], "result")
|
|
&& is_ident(&items[1], "string")
|
|
&& is_ident(&items[2], "i32")
|
|
{
|
|
return Some("(result string i32)".to_string());
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn render_supported_array_constructor_type(
|
|
form: &SExpr,
|
|
struct_names: &HashSet<String>,
|
|
enum_names: &HashSet<String>,
|
|
) -> Option<String> {
|
|
if is_ident(form, "i32") {
|
|
Some("i32".to_string())
|
|
} else if is_ident(form, "i64") {
|
|
Some("i64".to_string())
|
|
} else if is_ident(form, "u32") {
|
|
Some("u32".to_string())
|
|
} else if is_ident(form, "u64") {
|
|
Some("u64".to_string())
|
|
} else if is_ident(form, "f64") {
|
|
Some("f64".to_string())
|
|
} else if is_ident(form, "bool") {
|
|
Some("bool".to_string())
|
|
} else if is_ident(form, "string") {
|
|
Some("string".to_string())
|
|
} else if let Some(name) = expect_ident(form) {
|
|
if struct_names.contains(name) || enum_names.contains(name) {
|
|
Some(name.to_string())
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn render_supported_array_type(
|
|
items: &[SExpr],
|
|
struct_names: &HashSet<String>,
|
|
enum_names: &HashSet<String>,
|
|
) -> Option<String> {
|
|
if items.len() != 3 || !is_ident(&items[0], "array") {
|
|
return None;
|
|
}
|
|
|
|
let elem_ty = render_supported_array_constructor_type(&items[1], struct_names, enum_names)?;
|
|
let len = expect_int(&items[2])?;
|
|
if len <= 0 {
|
|
return None;
|
|
}
|
|
|
|
Some(format!("(array {} {})", elem_ty, len))
|
|
}
|
|
|
|
fn render_supported_vec_type(items: &[SExpr]) -> Option<String> {
|
|
if items.len() == 2 && is_ident(&items[0], "vec") {
|
|
if is_ident(&items[1], "i32") {
|
|
return Some("(vec i32)".to_string());
|
|
}
|
|
if is_ident(&items[1], "i64") {
|
|
return Some("(vec i64)".to_string());
|
|
}
|
|
if is_ident(&items[1], "f64") {
|
|
return Some("(vec f64)".to_string());
|
|
}
|
|
if is_ident(&items[1], "bool") {
|
|
return Some("(vec bool)".to_string());
|
|
}
|
|
if is_ident(&items[1], "string") {
|
|
return Some("(vec string)".to_string());
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn expect_list(form: &SExpr) -> Option<&[SExpr]> {
|
|
match &form.kind {
|
|
SExprKind::List(items) => Some(items),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn expect_ident(form: &SExpr) -> Option<&str> {
|
|
match &form.kind {
|
|
SExprKind::Atom(Atom::Ident(name)) => Some(name.as_str()),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn expect_int(form: &SExpr) -> Option<i64> {
|
|
match &form.kind {
|
|
SExprKind::Atom(Atom::Int(value)) => Some(*value),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn expect_string(form: &SExpr) -> Option<&str> {
|
|
match &form.kind {
|
|
SExprKind::Atom(Atom::String(value)) => Some(value.as_str()),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn is_ident(form: &SExpr, expected: &str) -> bool {
|
|
expect_ident(form).is_some_and(|actual| actual == expected)
|
|
}
|
|
|
|
fn is_c_symbol_name(value: &str) -> bool {
|
|
let mut chars = value.chars();
|
|
let Some(first) = chars.next() else {
|
|
return false;
|
|
};
|
|
(first == '_' || first.is_ascii_alphabetic())
|
|
&& chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
|
|
}
|
|
|
|
fn is_local_body_form(form: &SExpr) -> bool {
|
|
matches!(list_head(form), Some("let" | "var"))
|
|
}
|
|
|
|
fn is_sequential_test_body_form(form: &SExpr) -> bool {
|
|
matches!(list_head(form), Some("let" | "var" | "set" | "while"))
|
|
}
|
|
|
|
fn push_indented(output: &mut String, indent: &str, rendered: &str) {
|
|
for (index, line) in rendered.split('\n').enumerate() {
|
|
if index > 0 {
|
|
output.push('\n');
|
|
}
|
|
output.push_str(indent);
|
|
output.push_str(line);
|
|
}
|
|
}
|
|
|
|
fn is_valid_test_name(name: &str) -> bool {
|
|
!name.is_empty()
|
|
&& name
|
|
.bytes()
|
|
.all(|byte| matches!(byte, 0x20..=0x7e) && byte != b'"' && byte != b'\\')
|
|
}
|
|
|
|
fn render_string_literal(value: &str) -> String {
|
|
let mut output = String::from("\"");
|
|
for ch in value.chars() {
|
|
match ch {
|
|
'"' => output.push_str("\\\""),
|
|
'\\' => output.push_str("\\\\"),
|
|
'\n' => output.push_str("\\n"),
|
|
'\t' => output.push_str("\\t"),
|
|
ch if ch.is_control() => output.extend(ch.escape_default()),
|
|
ch => output.push(ch),
|
|
}
|
|
}
|
|
output.push('"');
|
|
output
|
|
}
|
|
|
|
fn render_float_literal(value: f64) -> String {
|
|
let rendered = value.to_string();
|
|
if rendered.contains('.') || rendered.contains('e') || rendered.contains('E') {
|
|
rendered
|
|
} else {
|
|
format!("{}.0", rendered)
|
|
}
|
|
}
|