use std::collections::{HashMap, HashSet}; use crate::{ diag::Diagnostic, reserved::{ is_unsupported_generic_standard_library_call, unsupported_generic_function, unsupported_generic_standard_library_call, unsupported_generic_type_alias, unsupported_reserved_type_diagnostic, }, sexpr::{Atom, SExpr, SExprKind}, std_runtime, token::Span, }; pub fn format(file: &str, source: &str, forms: &[SExpr]) -> Result> { let (comments, comment_errors) = collect_comments(source) .into_iter() .partition::, _>(|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), type_alias_names: collect_type_alias_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, next_comment: usize, output: String, function_names: HashSet, struct_names: HashSet, enum_names: HashSet, type_alias_names: HashSet, errors: Vec, } 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("type") => self.write_type_alias(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_type_alias(&mut self, form: &SExpr) { let Some(items) = expect_list(form) else { self.errors.push( Diagnostic::new( self.file, "MalformedTypeAlias", "type alias form must be a list", ) .with_span(form.span), ); return; }; if matches!(items.get(2).and_then(list_head), Some("type_params")) { self.errors.push(unsupported_generic_type_alias( self.file, items.get(2).map_or(form.span, |item| item.span), )); return; } if items.len() != 3 { self.errors.push( Diagnostic::new( self.file, "MalformedTypeAlias", "type alias form must be `(type Alias TargetType)`", ) .with_span(form.span), ); return; } let Some(name) = expect_ident(&items[1]) else { self.errors.push( Diagnostic::new( self.file, "InvalidTypeAliasName", "type alias name must be an identifier", ) .with_span(items[1].span), ); return; }; let Some(target) = self.render_alias_target_type(&items[2]) else { return; }; self.reject_comments_before( form.span.end, "formatter does not support comments inside type alias forms", ); self.output.push_str("(type "); self.output.push_str(name); self.output.push(' '); self.output.push_str(&target); self.output.push(')'); } 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) || self.type_alias_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, known type aliases, and known non-recursive struct enum payload variants", ) .with_span(variant_items[1].span) .expected("i32, i64, f64, bool, string, known type alias, 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, known type aliases, and known non-recursive struct enum payload variants", ) .with_span(variant_items[1].span) .expected("i32, i64, f64, bool, string, known type alias, 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; } if matches!(items.get(2).and_then(list_head), Some("type_params")) { self.errors .push(unsupported_generic_function(self.file, items[2].span)); 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> { 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) || self.type_alias_names.contains(name) { name.to_string() } else if self.push_reserved_type_error(&pair[1]) { 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; } } else if let Some(items) = expect_list(&pair[1]) { if let Some(text) = render_option_result_type(items, &self.type_alias_names) { text } else if let Some(text) = render_supported_array_type( items, &self.struct_names, &self.enum_names, &self.type_alias_names, ) { text } else if let Some(text) = render_supported_vec_type(items, &self.type_alias_names) { text } else if self.push_reserved_type_error(&pair[1]) { 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; } } 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 { 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) || self.type_alias_names.contains(name) { return Some(name.to_string()); } } if self.push_reserved_type_error(form) { return None; } 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, &self.type_alias_names) { return Some(text); } if let Some(text) = render_supported_array_type( items, &self.struct_names, &self.enum_names, &self.type_alias_names, ) { return Some(text); } if let Some(text) = render_supported_vec_type(items, &self.type_alias_names) { return Some(text); } if self.push_reserved_type_error(form) { return None; } 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_alias_target_type(&mut self, form: &SExpr) -> Option { if let Some(text) = render_direct_type_atom( form, &self.struct_names, &self.enum_names, &self.type_alias_names, ) { return Some(text); } if self.push_reserved_type_error(form) { return None; } let Some(items) = expect_list(form) else { self.errors.push( Diagnostic::new( self.file, "InvalidTypeAliasTarget", "type alias target type is invalid", ) .with_span(form.span) .expected("concrete type"), ); return None; }; if let Some(text) = render_option_result_type(items, &self.type_alias_names) { return Some(text); } if let Some(text) = render_supported_array_type( items, &self.struct_names, &self.enum_names, &self.type_alias_names, ) { return Some(text); } if let Some(text) = render_supported_vec_type(items, &self.type_alias_names) { return Some(text); } if self.push_reserved_type_error(form) { return None; } self.errors.push( Diagnostic::new( self.file, "InvalidTypeAliasTarget", "type alias target type is invalid", ) .with_span(form.span) .expected("concrete type"), ); None } fn render_c_import_return_type(&mut self, form: &SExpr) -> Option { 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) -> Option { 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) -> Option { 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 is_unsupported_generic_standard_library_call(other) => { self.errors.push(unsupported_generic_standard_library_call( self.file, items[0].span, other, )); None } 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, ) -> Option { 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 { 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, }); } if self.type_alias_names.contains(name) { return Some(RenderedType { text: name.to_string(), is_array: false, }); } } if self.push_reserved_type_error(form) { return None; } 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, &self.type_alias_names) { return Some(RenderedType { text, is_array: false, }); } if let Some(text) = render_supported_vec_type(items, &self.type_alias_names) { return Some(RenderedType { text, is_array: false, }); } if self.push_reserved_type_error(form) { return None; } 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, &self.type_alias_names, ) else { if self.push_reserved_type_error(&items[1]) { return None; } 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 { 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) || self.type_alias_names.contains(name) { return Some(name.to_string()); } } if self.push_reserved_type_error(form) { return None; } 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, &self.type_alias_names) { return Some(text); } if let Some(text) = render_supported_array_type( items, &self.struct_names, &self.enum_names, &self.type_alias_names, ) { return Some(text); } if let Some(text) = render_supported_vec_type(items, &self.type_alias_names) { return Some(text); } if self.push_reserved_type_error(form) { return None; } 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, ) -> Option { 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, ) -> Option { 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, ) -> Option { 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, ) -> Option { 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> { 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, ) -> Option { 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, ) -> Option { 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, ) -> Option { 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, ) -> Option { 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, &self.type_alias_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, ) -> Option { 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 Some(payload_type) = render_payload_type_atom(&items[1], &self.type_alias_names) else { if self.push_reserved_type_error(&items[1]) { return None; } 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, ) -> Option { 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 Some(ok_ty) = render_payload_type_atom(&items[1], &self.type_alias_names) else { if self.push_reserved_type_error(&items[1]) { return None; } 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 Some(err_ty) = render_result_err_type_atom(&items[2], &self.type_alias_names) else { if self.push_reserved_type_error(&items[2]) { return None; } 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, ok_ty, err_ty, value)) } fn render_call( &mut self, span: Span, name: &str, args: &[SExpr], env: &mut HashSet, exact_arity: usize, ) -> Option { 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 push_reserved_type_error(&mut self, form: &SExpr) -> bool { if let Some(diagnostic) = unsupported_reserved_type_diagnostic(self.file, form) { self.errors.push(diagnostic); true } else { false } } 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 { 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 { 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 { 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 { 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 { 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 collect_type_alias_names(forms: &[SExpr]) -> HashSet { forms .iter() .filter_map(|form| { let items = expect_list(form)?; if !is_ident(items.first()?, "type") { 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 { 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], type_alias_names: &HashSet, ) -> Option { if items.len() == 2 && is_ident(&items[0], "option") { let payload = render_payload_type_atom(&items[1], type_alias_names)?; return Some(format!("(option {})", payload)); } if items.len() == 3 && is_ident(&items[0], "result") { let ok = render_payload_type_atom(&items[1], type_alias_names)?; let err = render_result_err_type_atom(&items[2], type_alias_names)?; return Some(format!("(result {} {})", ok, err)); } None } fn render_supported_array_constructor_type( form: &SExpr, struct_names: &HashSet, enum_names: &HashSet, type_alias_names: &HashSet, ) -> Option { render_direct_type_atom(form, struct_names, enum_names, type_alias_names) } fn render_supported_array_type( items: &[SExpr], struct_names: &HashSet, enum_names: &HashSet, type_alias_names: &HashSet, ) -> Option { 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, type_alias_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], type_alias_names: &HashSet, ) -> Option { 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()); } if let Some(name) = expect_ident(&items[1]) { if type_alias_names.contains(name) { return Some(format!("(vec {})", name)); } } } None } fn render_payload_type_atom(form: &SExpr, type_alias_names: &HashSet) -> Option { 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) { type_alias_names.contains(name).then(|| name.to_string()) } else { None } } fn render_result_err_type_atom(form: &SExpr, type_alias_names: &HashSet) -> Option { if is_ident(form, "i32") { Some("i32".to_string()) } else if let Some(name) = expect_ident(form) { type_alias_names.contains(name).then(|| name.to_string()) } else { None } } fn render_direct_type_atom( form: &SExpr, struct_names: &HashSet, enum_names: &HashSet, type_alias_names: &HashSet, ) -> Option { if let Some(text) = render_payload_type_atom(form, type_alias_names) { return Some(text); } let name = expect_ident(form)?; if struct_names.contains(name) || enum_names.contains(name) { Some(name.to_string()) } else { 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 { 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) } }