slovo/compiler/src/formatter.rs
2026-05-22 08:38:43 +02:00

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(&params);
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 &params {
env.insert((*param).to_string());
}
self.output.push_str("(fn ");
self.output.push_str(name);
self.output.push(' ');
self.write_params(&params);
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)
}
}