use std::collections::hash_map::Entry; use std::collections::{HashMap, HashSet}; use crate::{ ast::{ BinaryOp, CImportDecl, EnumDecl, EnumVariantDecl, Expr, ExprKind, Function, MatchArm, MatchPattern, MatchPatternKind, Param, Program, StructDecl, StructField, StructInitField, Test, TypeAliasDecl, }, 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}, token::Span, types::Type, }; pub fn lower_program(file: &str, forms: &[SExpr]) -> Result> { lower_program_with_imported_names(file, forms, &[]) } pub fn lower_program_with_imported_names( file: &str, forms: &[SExpr], imported_names: &[String], ) -> Result> { let mut module = None; let mut type_aliases = Vec::new(); let mut alias_names = HashMap::new(); let mut enums = Vec::new(); let mut enum_names = imported_names.iter().cloned().collect::>(); let mut structs = Vec::new(); let mut struct_names = HashSet::new(); let mut c_imports = Vec::new(); let mut functions = Vec::new(); let mut tests = Vec::new(); let mut test_names = HashMap::new(); let mut errors = Vec::new(); for form in forms { match list_head(form) { Some("enum") => match lower_enum(file, form) { Ok(enum_decl) => { enum_names.insert(enum_decl.name.clone()); enums.push(enum_decl); } Err(mut errs) => errors.append(&mut errs), }, Some("struct") => match lower_struct(file, form) { Ok(struct_decl) => { struct_names.insert(struct_decl.name.clone()); structs.push(struct_decl); } Err(mut errs) => errors.append(&mut errs), }, Some("type") => match lower_type_alias(file, form) { Ok(alias) => match alias_names.entry(alias.name.clone()) { Entry::Vacant(entry) => { entry.insert(alias.name_span); type_aliases.push(alias); } Entry::Occupied(entry) => errors.push( Diagnostic::new( file, "DuplicateTypeAlias", format!("duplicate type alias `{}`", alias.name), ) .with_span(alias.name_span) .related("original type alias", *entry.get()), ), }, Err(mut errs) => errors.append(&mut errs), }, _ => {} } } for alias in &type_aliases { if is_reserved_type_name(&alias.name) { errors.push( Diagnostic::new( file, "TypeAliasNameConflict", format!( "type alias `{}` conflicts with a built-in type name", alias.name ), ) .with_span(alias.name_span) .hint("choose an alias name distinct from built-in type names"), ); } if let Some(struct_decl) = structs.iter().find(|decl| decl.name == alias.name) { errors.push( Diagnostic::new( file, "TypeAliasNameConflict", format!("type alias `{}` conflicts with a struct", alias.name), ) .with_span(alias.name_span) .related("struct declaration", struct_decl.name_span), ); } if let Some(enum_decl) = enums.iter().find(|decl| decl.name == alias.name) { errors.push( Diagnostic::new( file, "TypeAliasNameConflict", format!("type alias `{}` conflicts with an enum", alias.name), ) .with_span(alias.name_span) .related("enum declaration", enum_decl.name_span), ); } } for form in forms { match list_head(form) { Some("module") => match lower_module(file, form) { Ok(name) => module = Some(name), Err(err) => errors.push(err), }, Some("enum") => {} Some("struct") => {} Some("type") => {} Some("import_c") => match lower_c_import(file, form) { Ok(import) => c_imports.push(import), Err(mut errs) => errors.append(&mut errs), }, Some("fn") => match lower_function(file, form, &struct_names, &enum_names) { Ok(function) => functions.push(function), Err(mut errs) => errors.append(&mut errs), }, Some("test") => match lower_test(file, form, &struct_names, &enum_names) { Ok(test) => match test_names.entry(test.name.clone()) { Entry::Vacant(entry) => { entry.insert(test.name_span); tests.push(test); } Entry::Occupied(entry) => errors.push( Diagnostic::new( file, "DuplicateTestName", format!("duplicate test name `{}`", test.name), ) .with_span(test.name_span) .hint("test names must be unique within a module") .related("original test name", *entry.get()), ), }, Err(mut errs) => errors.append(&mut errs), }, Some(other) => errors.push( Diagnostic::new( file, "UnknownTopLevelForm", format!("unknown top-level form `{}`", other), ) .with_span(form.span), ), None => errors.push( Diagnostic::new(file, "ExpectedTopLevelForm", "expected top-level form") .with_span(form.span), ), } } if errors.is_empty() { Ok(Program { module: module.unwrap_or_else(|| "main".to_string()), type_aliases, enums, structs, c_imports, functions, tests, }) } else { Err(errors) } } pub fn print_program(program: &Program) -> String { let mut output = String::new(); output.push_str("program "); output.push_str(&program.module); output.push('\n'); for alias in &program.type_aliases { output.push_str(" type "); output.push_str(&alias.name); output.push_str(" = "); output.push_str(&alias.target.to_string()); output.push('\n'); } for enum_decl in &program.enums { output.push_str(" enum "); output.push_str(&enum_decl.name); output.push('\n'); for variant in &enum_decl.variants { output.push_str(" variant "); output.push_str(&variant.name); if let Some(payload_ty) = &variant.payload_ty { output.push(' '); output.push_str(&payload_ty.to_string()); } output.push('\n'); } } for struct_decl in &program.structs { output.push_str(" struct "); output.push_str(&struct_decl.name); output.push('\n'); for field in &struct_decl.fields { output.push_str(" field "); output.push_str(&field.name); output.push_str(": "); output.push_str(&field.ty.to_string()); output.push('\n'); } } for import in &program.c_imports { output.push_str(" import_c "); output.push_str(&import.name); output.push('('); for (index, param) in import.params.iter().enumerate() { if index > 0 { output.push_str(", "); } output.push_str(¶m.name); output.push_str(": "); output.push_str(¶m.ty.to_string()); } output.push_str(") -> "); output.push_str(&import.return_type.to_string()); output.push('\n'); } for function in &program.functions { output.push_str(" fn "); output.push_str(&function.name); output.push('('); for (index, param) in function.params.iter().enumerate() { if index > 0 { output.push_str(", "); } output.push_str(¶m.name); output.push_str(": "); output.push_str(¶m.ty.to_string()); } output.push_str(") -> "); output.push_str(&function.return_type.to_string()); output.push('\n'); for expr in &function.body { write_expr(expr, 2, &mut output); } } for test in &program.tests { output.push_str(" test \""); for ch in test.name.chars() { output.extend(ch.escape_default()); } output.push_str("\"\n"); for expr in &test.body { write_expr(expr, 2, &mut output); } } output } fn write_expr(expr: &Expr, indent: usize, output: &mut String) { output.push_str(&" ".repeat(indent)); match &expr.kind { ExprKind::Int(value) => { output.push_str("int "); output.push_str(&value.to_string()); output.push('\n'); } ExprKind::Int64(value) => { output.push_str("i64 "); output.push_str(&value.to_string()); output.push('\n'); } ExprKind::UInt32(value) => { output.push_str("u32 "); output.push_str(&value.to_string()); output.push('\n'); } ExprKind::UInt64(value) => { output.push_str("u64 "); output.push_str(&value.to_string()); output.push('\n'); } ExprKind::Float(value) => { output.push_str("float "); output.push_str(&value.to_string()); output.push('\n'); } ExprKind::Bool(value) => { output.push_str("bool "); output.push_str(&value.to_string()); output.push('\n'); } ExprKind::String(value) => { output.push_str("string \""); for ch in value.chars() { output.extend(ch.escape_default()); } output.push_str("\"\n"); } ExprKind::Var(name) => { output.push_str("var "); output.push_str(name); output.push('\n'); } ExprKind::StructInit { name, fields, .. } => { output.push_str("construct "); output.push_str(name); output.push('\n'); for field in fields { output.push_str(&" ".repeat(indent + 1)); output.push_str("field "); output.push_str(&field.name); output.push('\n'); write_expr(&field.expr, indent + 2, output); } } ExprKind::ArrayInit { elem_ty, elements, .. } => { output.push_str("array "); output.push_str(&elem_ty.to_string()); output.push('\n'); for element in elements { write_expr(element, indent + 1, output); } } ExprKind::OptionSome { payload_ty, value, .. } => { output.push_str("some "); output.push_str(&payload_ty.to_string()); output.push('\n'); write_expr(value, indent + 1, output); } ExprKind::OptionNone { payload_ty, .. } => { output.push_str("none "); output.push_str(&payload_ty.to_string()); output.push('\n'); } ExprKind::ResultOk { ok_ty, err_ty, value, .. } => { output.push_str("ok "); output.push_str(&ok_ty.to_string()); output.push(' '); output.push_str(&err_ty.to_string()); output.push('\n'); write_expr(value, indent + 1, output); } ExprKind::ResultErr { ok_ty, err_ty, value, .. } => { output.push_str("err "); output.push_str(&ok_ty.to_string()); output.push(' '); output.push_str(&err_ty.to_string()); output.push('\n'); write_expr(value, indent + 1, output); } ExprKind::OptionIsSome { value } => { output.push_str("is_some\n"); write_expr(value, indent + 1, output); } ExprKind::OptionIsNone { value } => { output.push_str("is_none\n"); write_expr(value, indent + 1, output); } ExprKind::OptionUnwrapSome { value } => { output.push_str("unwrap_some\n"); write_expr(value, indent + 1, output); } ExprKind::ResultIsOk { source_name, value } => { output.push_str(source_name); output.push('\n'); write_expr(value, indent + 1, output); } ExprKind::ResultIsErr { source_name, value } => { output.push_str(source_name); output.push('\n'); write_expr(value, indent + 1, output); } ExprKind::ResultUnwrapOk { source_name, value } => { output.push_str(source_name); output.push('\n'); write_expr(value, indent + 1, output); } ExprKind::ResultUnwrapErr { source_name, value } => { output.push_str(source_name); output.push('\n'); write_expr(value, indent + 1, output); } ExprKind::EnumVariant { enum_name, variant, args, .. } => { output.push_str("enum-variant "); output.push_str(enum_name); output.push('.'); output.push_str(variant); output.push('\n'); for arg in args { write_expr(arg, indent + 1, output); } } ExprKind::FieldAccess { value, field, .. } => { output.push_str("field-access "); output.push_str(field); output.push('\n'); write_expr(value, indent + 1, output); } ExprKind::Index { array, index } => { output.push_str("index\n"); write_expr(array, indent + 1, output); write_expr(index, indent + 1, output); } ExprKind::Local { mutable, name, ty, expr, .. } => { output.push_str(if *mutable { "local var " } else { "local let " }); output.push_str(name); output.push_str(": "); output.push_str(&ty.to_string()); output.push('\n'); write_expr(expr, indent + 1, output); } ExprKind::Set { name, expr, .. } => { output.push_str("set "); output.push_str(name); output.push('\n'); write_expr(expr, indent + 1, output); } ExprKind::Binary { op, left, right } => { output.push_str("binary "); output.push_str(binary_op_name(*op)); output.push('\n'); write_expr(left, indent + 1, output); write_expr(right, indent + 1, output); } ExprKind::If { condition, then_expr, else_expr, } => { output.push_str("if\n"); write_expr(condition, indent + 1, output); write_expr(then_expr, indent + 1, output); write_expr(else_expr, indent + 1, output); } ExprKind::Match { subject, arms } => { output.push_str("match\n"); output.push_str(&" ".repeat(indent + 1)); output.push_str("subject\n"); write_expr(subject, indent + 2, output); for arm in arms { output.push_str(&" ".repeat(indent + 1)); output.push_str("arm "); output.push_str(&match_pattern_name(&arm.pattern.kind)); if let Some(binding) = &arm.pattern.binding { output.push(' '); output.push_str(binding); } output.push('\n'); for expr in &arm.body { write_expr(expr, indent + 2, output); } } } ExprKind::While { condition, body } => { output.push_str("while\n"); write_expr(condition, indent + 1, output); for expr in body { write_expr(expr, indent + 1, output); } } ExprKind::Unsafe { body } => { output.push_str("unsafe\n"); for expr in body { write_expr(expr, indent + 1, output); } } ExprKind::Call { name, args, .. } => { output.push_str("call "); output.push_str(name); output.push('\n'); for arg in args { write_expr(arg, indent + 1, output); } } } } pub fn match_pattern_name(kind: &MatchPatternKind) -> String { match kind { MatchPatternKind::Some => "some".to_string(), MatchPatternKind::None => "none".to_string(), MatchPatternKind::Ok => "ok".to_string(), MatchPatternKind::Err => "err".to_string(), MatchPatternKind::EnumVariant { enum_name, variant } => { format!("{}.{}", enum_name, variant) } } } pub fn binary_op_name(op: BinaryOp) -> &'static str { match op { BinaryOp::Add => "+", BinaryOp::Sub => "-", BinaryOp::Mul => "*", BinaryOp::Div => "/", BinaryOp::Rem => "%", BinaryOp::BitAnd => "bit_and", BinaryOp::BitOr => "bit_or", BinaryOp::BitXor => "bit_xor", BinaryOp::Eq => "=", BinaryOp::Lt => "<", BinaryOp::Gt => ">", BinaryOp::Le => "<=", BinaryOp::Ge => ">=", } } fn lower_module(file: &str, form: &SExpr) -> Result { let items = expect_list(form).ok_or_else(|| { Diagnostic::new(file, "ExpectedList", "expected module list").with_span(form.span) })?; if items.len() != 2 && items.len() != 3 { return Err(Diagnostic::new( file, "InvalidModule", "module form must be `(module name)` or `(module name (export ...))`", ) .with_span(form.span)); } if let Some(export_form) = items.get(2) { let export_items = expect_list(export_form).ok_or_else(|| { Diagnostic::new( file, "InvalidExport", "export list must be `(export name...)`", ) .with_span(export_form.span) })?; if !matches!(export_items.first().and_then(expect_ident), Some("export")) { return Err(Diagnostic::new( file, "InvalidExport", "module option must be an export list", ) .with_span(export_form.span)); } let mut seen = HashMap::new(); for item in &export_items[1..] { let Some(name) = expect_ident(item) else { return Err(Diagnostic::new( file, "InvalidExport", "exported name must be an identifier", ) .with_span(item.span)); }; if seen.insert(name.to_string(), item.span).is_some() { return Err(Diagnostic::new( file, "DuplicateName", format!("duplicate exported name `{}`", name), ) .with_span(item.span)); } } } expect_ident(&items[1]) .map(|s| s.to_string()) .ok_or_else(|| { Diagnostic::new( file, "InvalidModuleName", "module name must be an identifier", ) .with_span(items[1].span) }) } fn lower_type_alias(file: &str, form: &SExpr) -> Result> { let mut errors = Vec::new(); let Some(items) = expect_list(form) else { return Err(vec![Diagnostic::new( file, "MalformedTypeAlias", "type alias form must be a list", ) .with_span(form.span)]); }; if matches!(items.get(2).and_then(list_head), Some("type_params")) { return Err(vec![unsupported_generic_type_alias( file, items.get(2).map_or(form.span, |item| item.span), )]); } if items.len() != 3 { return Err(vec![Diagnostic::new( file, "MalformedTypeAlias", "type alias form must be `(type Alias TargetType)`", ) .with_span(form.span) .expected("(type Alias TargetType)")]); } let name = match expect_ident(&items[1]) { Some(name) => name.to_string(), None => { errors.push( Diagnostic::new( file, "InvalidTypeAliasName", "type alias name must be an identifier", ) .with_span(items[1].span), ); "".to_string() } }; let target = match lower_type(&items[2]) { Some(ty) => ty, None => { errors.push(invalid_type_diagnostic( file, &items[2], "InvalidTypeAliasTarget", "type alias target type is invalid", Some("concrete type"), None, )); Type::Unit } }; if errors.is_empty() { Ok(TypeAliasDecl { name, name_span: items[1].span, target, target_span: items[2].span, span: form.span, }) } else { Err(errors) } } fn lower_struct(file: &str, form: &SExpr) -> Result> { let mut errors = Vec::new(); let Some(items) = expect_list(form) else { return Err(vec![Diagnostic::new( file, "ExpectedList", "expected struct list", ) .with_span(form.span)]); }; if items.len() < 2 { return Err(vec![Diagnostic::new( file, "MalformedStructForm", "struct form must be `(struct Name (field type)...)`", ) .with_span(form.span)]); } let name = match expect_ident(&items[1]) { Some(name) => name.to_string(), None => { errors.push( Diagnostic::new( file, "InvalidStructName", "struct name must be an identifier", ) .with_span(items[1].span), ); "".to_string() } }; let mut fields = Vec::new(); for item in &items[2..] { let Some(pair) = expect_list(item) else { errors.push( Diagnostic::new( file, "InvalidStructField", "struct field must be `(name type)`", ) .with_span(item.span), ); continue; }; if pair.len() != 2 { errors.push( Diagnostic::new( file, "InvalidStructField", "struct field must be `(name type)`", ) .with_span(item.span), ); continue; } let Some(field_name) = expect_ident(&pair[0]) else { errors.push( Diagnostic::new( file, "InvalidStructFieldName", "struct field name must be an identifier", ) .with_span(pair[0].span), ); continue; }; let Some(ty) = lower_type(&pair[1]) else { errors.push(invalid_type_diagnostic( file, &pair[1], "InvalidStructFieldType", "invalid struct field type", None, None, )); continue; }; fields.push(StructField { name: field_name.to_string(), name_span: pair[0].span, ty, ty_span: pair[1].span, }); } if errors.is_empty() { Ok(StructDecl { name, name_span: items[1].span, fields, span: form.span, }) } else { Err(errors) } } fn lower_enum(file: &str, form: &SExpr) -> Result> { let mut errors = Vec::new(); let Some(items) = expect_list(form) else { return Err(vec![Diagnostic::new( file, "ExpectedList", "expected enum list", ) .with_span(form.span)]); }; if items.len() < 2 { return Err(vec![Diagnostic::new( file, "MalformedEnumForm", "enum form must be `(enum Name Variant...)`", ) .with_span(form.span)]); } let name = match expect_ident(&items[1]) { Some(name) => name.to_string(), None => { errors.push( Diagnostic::new(file, "InvalidEnumName", "enum name must be an identifier") .with_span(items[1].span), ); "".to_string() } }; let mut variants = Vec::new(); for item in &items[2..] { match expect_ident(item) { Some(name) => variants.push(EnumVariantDecl { name: name.to_string(), name_span: item.span, payload_ty: None, payload_ty_span: None, }), None => { let Some(variant_items) = expect_list(item) else { errors.push( Diagnostic::new( file, "InvalidEnumVariant", "enum variants must be identifiers or unary payload forms", ) .with_span(item.span) .expected("Variant or (Variant i32)"), ); continue; }; if variant_items.len() != 2 { errors.push( Diagnostic::new( file, "InvalidEnumVariant", "enum payload variants must be unary forms", ) .with_span(item.span) .expected("(Variant i32)"), ); continue; } let Some(name) = expect_ident(&variant_items[0]) else { errors.push( Diagnostic::new( file, "InvalidEnumVariant", "enum variant name must be an identifier", ) .with_span(variant_items[0].span), ); continue; }; let Some(payload_ty) = lower_type(&variant_items[1]) else { errors.push(invalid_type_diagnostic( file, &variant_items[1], "InvalidEnumVariant", "enum variant payload type is invalid", Some("i32"), None, )); continue; }; variants.push(EnumVariantDecl { name: name.to_string(), name_span: variant_items[0].span, payload_ty: Some(payload_ty), payload_ty_span: Some(variant_items[1].span), }); } } } if errors.is_empty() { Ok(EnumDecl { name, name_span: items[1].span, variants, span: form.span, }) } else { Err(errors) } } fn lower_function( file: &str, form: &SExpr, struct_names: &HashSet, enum_names: &HashSet, ) -> Result> { let mut errors = Vec::new(); let Some(items) = expect_list(form) else { return Err(vec![Diagnostic::new( file, "ExpectedList", "expected function list", ) .with_span(form.span)]); }; if items.len() < 6 { return Err(vec![Diagnostic::new( file, "InvalidFunction", "function form is incomplete", ) .with_span(form.span) .hint("expected `(fn name ((arg Type) ...) -> ReturnType body...)`")]); } if matches!(items.get(2).and_then(list_head), Some("type_params")) { return Err(vec![unsupported_generic_function(file, items[2].span)]); } let name = match expect_ident(&items[1]) { Some(name) => name.to_string(), None => { errors.push( Diagnostic::new( file, "InvalidFunctionName", "function name must be an identifier", ) .with_span(items[1].span), ); "".to_string() } }; let params = match lower_params(file, &items[2]) { Ok(params) => params, Err(mut errs) => { errors.append(&mut errs); Vec::new() } }; if !matches!(items[3].kind, SExprKind::Atom(Atom::Arrow)) { errors.push( Diagnostic::new(file, "ExpectedArrow", "expected `->` in function signature") .with_span(items[3].span), ); } let return_type = match lower_type(&items[4]) { Some(Type::Unit) => { errors.push(unsupported_unit_return_signature(file, items[4].span)); Type::Unit } Some(ty) => ty, None => { errors.push(invalid_type_diagnostic( file, &items[4], "InvalidReturnType", "invalid return type", None, None, )); Type::Unit } }; let mut body = Vec::new(); for expr in &items[5..] { match lower_expr(file, expr, struct_names, enum_names) { Ok(expr) => body.push(expr), Err(err) => errors.push(err), } } if errors.is_empty() { Ok(Function { name, params, return_type, return_type_span: items[4].span, body, span: form.span, }) } else { Err(errors) } } fn lower_c_import(file: &str, form: &SExpr) -> Result> { let mut errors = Vec::new(); let Some(items) = expect_list(form) else { return Err(vec![Diagnostic::new( file, "MalformedCImport", "`import_c` declaration must be a list", ) .with_span(form.span)]); }; if items.len() != 5 { return Err(vec![Diagnostic::new( file, "MalformedCImport", "`import_c` form must be `(import_c name ((arg i32)...) -> ReturnType)`", ) .with_span(form.span) .expected("(import_c name ((arg i32)...) -> i32)")]); } let name = match expect_ident(&items[1]) { Some(name) => name.to_string(), None => { errors.push( Diagnostic::new( file, "MalformedCImport", "C import name must be an identifier", ) .with_span(items[1].span), ); "".to_string() } }; if name != "" && !is_c_symbol_name(&name) { errors.push( Diagnostic::new( 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.clone()), ); } let params = match lower_c_import_params(file, &items[2]) { Ok(params) => params, Err(mut errs) => { errors.append(&mut errs); Vec::new() } }; if !matches!(items[3].kind, SExprKind::Atom(Atom::Arrow)) { errors.push( Diagnostic::new( file, "MalformedCImport", "expected `->` in C import signature", ) .with_span(items[3].span), ); } let return_type = match lower_type(&items[4]) { Some(ty) => ty, None => { errors.push(invalid_type_diagnostic( file, &items[4], "MalformedCImport", "invalid C import return type", None, None, )); Type::Unit } }; for param in ¶ms { if param.ty != Type::I32 { errors.push( Diagnostic::new( file, "UnsupportedCImportType", "C import parameters support only `i32` in exp-6", ) .with_span(param.ty_span) .expected(Type::I32.to_string()) .found(param.ty.to_string()), ); } } if return_type != Type::I32 && return_type != Type::Unit { errors.push( Diagnostic::new( file, "UnsupportedCImportType", "C import return type must be `i32` or `unit` in exp-6", ) .with_span(items[4].span) .expected("i32 or unit") .found(return_type.to_string()), ); } if errors.is_empty() { Ok(CImportDecl { name, name_span: items[1].span, params, return_type, return_type_span: items[4].span, span: form.span, }) } else { Err(errors) } } fn lower_c_import_params(file: &str, form: &SExpr) -> Result, Vec> { let mut errors = Vec::new(); let Some(items) = expect_list(form) else { return Err(vec![Diagnostic::new( file, "MalformedCImport", "C import parameters must be a list", ) .with_span(form.span)]); }; let mut params = Vec::new(); let mut seen = HashMap::::new(); for item in items { let Some(pair) = expect_list(item) else { errors.push( Diagnostic::new( file, "MalformedCImport", "C import parameter must be `(name Type)`", ) .with_span(item.span), ); continue; }; if pair.len() != 2 { errors.push( Diagnostic::new( file, "MalformedCImport", "C import parameter must be `(name Type)`", ) .with_span(item.span), ); continue; } let Some(name) = expect_ident(&pair[0]) else { errors.push( Diagnostic::new( file, "MalformedCImport", "C import parameter name must be an identifier", ) .with_span(pair[0].span), ); continue; }; if let Some(original) = seen.insert(name.to_string(), pair[0].span) { errors.push( Diagnostic::new( file, "DuplicateName", format!("duplicate C import parameter `{}`", name), ) .with_span(pair[0].span) .related("original parameter", original), ); } let Some(ty) = lower_type(&pair[1]) else { errors.push(invalid_type_diagnostic( file, &pair[1], "MalformedCImport", "invalid C import parameter type", None, None, )); continue; }; params.push(Param { name: name.to_string(), name_span: pair[0].span, ty, ty_span: pair[1].span, }); } if errors.is_empty() { Ok(params) } else { Err(errors) } } 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 lower_test( file: &str, form: &SExpr, struct_names: &HashSet, enum_names: &HashSet, ) -> Result> { let mut errors = Vec::new(); let Some(items) = expect_list(form) else { return Err(vec![Diagnostic::new( file, "ExpectedList", "expected test list", ) .with_span(form.span)]); }; if items.len() < 3 { return Err(vec![Diagnostic::new( file, "MalformedTestForm", "test form must be `(test \"name\" body...)`", ) .with_span(form.span) .hint("use one string name followed by a bool result expression")]); } let name = match expect_string(&items[1]) { Some(name) if valid_test_name(name) => name.to_string(), Some(_) => { errors.push( Diagnostic::new( file, "InvalidTestName", "test name must be non-empty printable ASCII without quotes, backslashes, or newlines", ) .with_span(items[1].span) .expected("non-empty printable ASCII without quotes, backslashes, or newlines"), ); "".to_string() } None => { errors.push( Diagnostic::new( file, "InvalidTestName", "test name must be a string literal", ) .with_span(items[1].span) .expected("string"), ); "".to_string() } }; let mut body = Vec::new(); for item in &items[2..] { match lower_expr(file, item, struct_names, enum_names) { Ok(expr) => body.push(expr), Err(err) => { errors.push(err); } } } if body.len() > 1 { for expr in &body[..body.len() - 1] { if !matches!( expr.kind, ExprKind::Local { .. } | ExprKind::Set { .. } | ExprKind::While { .. } ) { errors.push( Diagnostic::new( 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"), ); } } } if errors.is_empty() { Ok(Test { name, name_span: items[1].span, body, span: form.span, }) } else { Err(errors) } } fn lower_params(file: &str, form: &SExpr) -> Result, Vec> { let mut errors = Vec::new(); let Some(items) = expect_list(form) else { return Err(vec![Diagnostic::new( file, "InvalidParams", "parameters must be a list", ) .with_span(form.span)]); }; let mut params = Vec::new(); for item in items { let Some(pair) = expect_list(item) else { errors.push( Diagnostic::new(file, "InvalidParam", "parameter must be `(name Type)`") .with_span(item.span), ); continue; }; if pair.len() != 2 { errors.push( Diagnostic::new(file, "InvalidParam", "parameter must be `(name Type)`") .with_span(item.span), ); continue; } let Some(name) = expect_ident(&pair[0]) else { errors.push( Diagnostic::new( file, "InvalidParamName", "parameter name must be an identifier", ) .with_span(pair[0].span), ); continue; }; let Some(ty) = lower_type(&pair[1]) else { errors.push(invalid_type_diagnostic( file, &pair[1], "InvalidParamType", "invalid parameter type", None, None, )); continue; }; if ty == Type::Unit { errors.push(unsupported_unit_parameter_signature(file, pair[1].span)); continue; } params.push(Param { name: name.to_string(), name_span: pair[0].span, ty, ty_span: pair[1].span, }); } if errors.is_empty() { Ok(params) } else { Err(errors) } } fn unsupported_unit_parameter_signature(file: &str, span: crate::token::Span) -> Diagnostic { Diagnostic::new( file, "UnsupportedUnitSignatureType", "function parameter type `unit` is unsupported", ) .with_span(span) .expected("non-unit function parameter type") .found(Type::Unit.to_string()) .hint("`unit` is reserved for compiler/runtime unit-producing forms") } fn unsupported_unit_return_signature(file: &str, span: crate::token::Span) -> Diagnostic { Diagnostic::new( file, "UnsupportedUnitSignatureType", "function return type `unit` is unsupported", ) .with_span(span) .expected("non-unit function return type") .found(Type::Unit.to_string()) .hint("`unit` is reserved for compiler/runtime unit-producing forms") } fn is_reserved_type_name(name: &str) -> bool { matches!( name, "i32" | "i64" | "u32" | "u64" | "f64" | "bool" | "unit" | "string" | "ptr" | "slice" | "option" | "result" | "array" | "vec" | "map" | "set" ) } fn lower_type(form: &SExpr) -> Option { match &form.kind { SExprKind::Atom(Atom::Ident(name)) => match name.as_str() { "i32" => Some(Type::I32), "i64" => Some(Type::I64), "u32" => Some(Type::U32), "u64" => Some(Type::U64), "f64" => Some(Type::F64), "bool" => Some(Type::Bool), "unit" => Some(Type::Unit), "string" => Some(Type::String), other => Some(Type::Named(other.to_string())), }, SExprKind::List(items) if !items.is_empty() => { let head = expect_ident(&items[0])?; match head { "ptr" if items.len() == 2 => Some(Type::Ptr(Box::new(lower_type(&items[1])?))), "slice" if items.len() == 2 => Some(Type::Slice(Box::new(lower_type(&items[1])?))), "option" if items.len() == 2 => { Some(Type::Option(Box::new(lower_type(&items[1])?))) } "result" if items.len() == 3 => Some(Type::Result( Box::new(lower_type(&items[1])?), Box::new(lower_type(&items[2])?), )), "array" if items.len() == 3 => { let inner = lower_type(&items[1])?; let n = match items[2].kind { SExprKind::Atom(Atom::Int(n)) if n >= 0 => n as usize, _ => return None, }; Some(Type::Array(Box::new(inner), n)) } "vec" if items.len() == 2 => Some(Type::Vec(Box::new(lower_type(&items[1])?))), _ => None, } } _ => None, } } fn lower_expr( file: &str, form: &SExpr, struct_names: &HashSet, enum_names: &HashSet, ) -> Result { match &form.kind { SExprKind::Atom(Atom::Int(value)) => { let value = i32::try_from(*value).map_err(|_| { Diagnostic::new( file, "IntegerOutOfRange", "integer literal is outside the supported i32 range", ) .with_span(form.span) .expected("i32") .found(value.to_string()) })?; Ok(Expr { kind: ExprKind::Int(value), span: form.span, }) } SExprKind::Atom(Atom::I64(value)) => Ok(Expr { kind: ExprKind::Int64(*value), span: form.span, }), SExprKind::Atom(Atom::U32(value)) => Ok(Expr { kind: ExprKind::UInt32(*value), span: form.span, }), SExprKind::Atom(Atom::U64(value)) => Ok(Expr { kind: ExprKind::UInt64(*value), span: form.span, }), SExprKind::Atom(Atom::String(value)) => Ok(Expr { kind: ExprKind::String(value.clone()), span: form.span, }), SExprKind::Atom(Atom::Float(value)) => { if !value.is_finite() { return Err(Diagnostic::new( file, "UnsupportedFloatLiteral", "f64 literals must be finite in exp-20", ) .with_span(form.span) .expected("finite f64 literal") .found(value.to_string()) .hint("NaN and infinity semantics remain deferred")); } Ok(Expr { kind: ExprKind::Float(*value), span: form.span, }) } SExprKind::Atom(Atom::Ident(name)) if name == "true" => Ok(Expr { kind: ExprKind::Bool(true), span: form.span, }), SExprKind::Atom(Atom::Ident(name)) if name == "false" => Ok(Expr { kind: ExprKind::Bool(false), span: form.span, }), SExprKind::Atom(Atom::Ident(name)) => Ok(Expr { kind: ExprKind::Var(name.clone()), span: form.span, }), SExprKind::List(items) if items.is_empty() => { Err(Diagnostic::new(file, "EmptyForm", "empty form").with_span(form.span)) } SExprKind::List(items) => { let Some(head) = expect_ident(&items[0]) else { return Err(Diagnostic::new( file, "InvalidCall", "form head must be an identifier", ) .with_span(items[0].span)); }; match head { "+" | "-" | "*" | "/" | "%" | "bit_and" | "bit_or" | "bit_xor" | "=" | "<" | ">" | "<=" | ">=" => { if items.len() != 3 { return Err(Diagnostic::new( file, "InvalidBinary", "binary operator expects exactly two operands", ) .with_span(form.span)); } let op = match head { "+" => BinaryOp::Add, "-" => BinaryOp::Sub, "*" => BinaryOp::Mul, "/" => BinaryOp::Div, "%" => BinaryOp::Rem, "bit_and" => BinaryOp::BitAnd, "bit_or" => BinaryOp::BitOr, "bit_xor" => BinaryOp::BitXor, "=" => BinaryOp::Eq, "<" => BinaryOp::Lt, ">" => BinaryOp::Gt, "<=" => BinaryOp::Le, ">=" => BinaryOp::Ge, _ => unreachable!(), }; Ok(Expr { kind: ExprKind::Binary { op, left: Box::new(lower_expr(file, &items[1], struct_names, enum_names)?), right: Box::new(lower_expr(file, &items[2], struct_names, enum_names)?), }, span: form.span, }) } "and" | "or" => { if items.len() != 3 { return Err(Diagnostic::new( file, "InvalidLogical", "logical operator expects exactly two operands", ) .with_span(form.span)); } let left = lower_expr(file, &items[1], struct_names, enum_names)?; let right = lower_expr(file, &items[2], struct_names, enum_names)?; let literal = Expr { kind: ExprKind::Bool(head == "or"), span: form.span, }; let (then_expr, else_expr) = if head == "and" { (right, literal) } else { (literal, right) }; Ok(Expr { kind: ExprKind::If { condition: Box::new(left), then_expr: Box::new(then_expr), else_expr: Box::new(else_expr), }, span: form.span, }) } "not" => { if items.len() != 2 { return Err(Diagnostic::new( file, "InvalidLogical", "logical not expects exactly one operand", ) .with_span(form.span)); } Ok(Expr { kind: ExprKind::If { condition: Box::new(lower_expr( file, &items[1], struct_names, enum_names, )?), then_expr: Box::new(Expr { kind: ExprKind::Bool(false), span: form.span, }), else_expr: Box::new(Expr { kind: ExprKind::Bool(true), span: form.span, }), }, span: form.span, }) } "." => lower_field_access(file, form, items, struct_names, enum_names), "array" => lower_array_init(file, form, items, struct_names, enum_names), "some" => lower_option_some(file, form, items, struct_names, enum_names), "none" => lower_option_none(file, form, items), "ok" => lower_result_ok(file, form, items, struct_names, enum_names), "err" => lower_result_err(file, form, items, struct_names, enum_names), "is_some" | "is_none" | "is_ok" | "is_err" | "std.result.is_ok" | "std.result.is_err" => { lower_unary_observer(file, form, items, struct_names, enum_names, head) } "unwrap_some" | "unwrap_ok" | "unwrap_err" | "std.result.unwrap_ok" | "std.result.unwrap_err" => { lower_unary_payload_access(file, form, items, struct_names, enum_names, head) } "index" => lower_index(file, form, items, struct_names, enum_names), "if" => { if items.len() != 4 { return Err(Diagnostic::new( file, "MalformedIfForm", "`if` expects condition, then expression, else expression", ) .with_span(form.span) .hint("use `(if condition then-expression else-expression)`")); } Ok(Expr { kind: ExprKind::If { condition: Box::new(lower_expr( file, &items[1], struct_names, enum_names, )?), then_expr: Box::new(lower_expr( file, &items[2], struct_names, enum_names, )?), else_expr: Box::new(lower_expr( file, &items[3], struct_names, enum_names, )?), }, span: form.span, }) } "match" => lower_match(file, form, items, struct_names, enum_names), "let" | "var" => { lower_local(file, form, items, head == "var", struct_names, enum_names) } "set" => lower_set(file, form, items, struct_names, enum_names), "while" => lower_while(file, form, items, struct_names, enum_names), "unsafe" => lower_unsafe(file, form, items, struct_names, enum_names), name if struct_names.contains(name) => { lower_struct_init(file, form, items, name, struct_names, enum_names) } name if qualified_enum_name(name) .map(|(enum_name, _)| { enum_names.contains(enum_name) || starts_uppercase(enum_name) }) .unwrap_or(false) => { let (enum_name, variant) = qualified_enum_name(name).expect("qualified enum"); let mut args = Vec::new(); for item in &items[1..] { args.push(lower_expr(file, item, struct_names, enum_names)?); } Ok(Expr { kind: ExprKind::EnumVariant { enum_name: enum_name.to_string(), variant: variant.to_string(), name_span: items[0].span, args, }, span: form.span, }) } name if is_unsupported_generic_standard_library_call(name) => Err( unsupported_generic_standard_library_call(file, items[0].span, name), ), name => { let mut args = Vec::new(); for item in &items[1..] { args.push(lower_expr(file, item, struct_names, enum_names)?); } Ok(Expr { kind: ExprKind::Call { name: name.to_string(), name_span: items[0].span, args, }, span: form.span, }) } } } _ => Err( Diagnostic::new(file, "InvalidExpression", "invalid expression").with_span(form.span), ), } } fn lower_match( file: &str, form: &SExpr, items: &[SExpr], struct_names: &HashSet, enum_names: &HashSet, ) -> Result { if items.len() < 2 { return Err( Diagnostic::new(file, "MalformedMatchPattern", "`match` expects a subject") .with_span(form.span) .expected("(match subject (pattern body...) (pattern body...))"), ); } let subject = Box::new(lower_expr(file, &items[1], struct_names, enum_names)?); let mut arms = Vec::new(); for item in &items[2..] { let Some(arm_items) = expect_list(item) else { return Err(Diagnostic::new( file, "MalformedMatchPattern", "match arm must be a list containing a pattern and body", ) .with_span(item.span) .expected("((some payload) body...)")); }; if arm_items.len() < 2 { return Err(Diagnostic::new( file, "MalformedMatchPattern", "match arm must contain a pattern and at least one body expression", ) .with_span(item.span) .expected("((some payload) body...)")); } let pattern = lower_match_pattern(file, &arm_items[0])?; let mut body = Vec::new(); for body_expr in &arm_items[1..] { body.push(lower_expr(file, body_expr, struct_names, enum_names)?); } arms.push(MatchArm { pattern, body, span: item.span, }); } Ok(Expr { kind: ExprKind::Match { subject, arms }, span: form.span, }) } fn lower_match_pattern(file: &str, form: &SExpr) -> Result { let Some(items) = expect_list(form) else { return Err(Diagnostic::new( file, "MalformedMatchPattern", "match arm pattern must be a list", ) .with_span(form.span) .expected("(some binding), (none), (ok binding), or (err binding)")); }; let Some(head) = items.first().and_then(expect_ident) else { return Err(Diagnostic::new( file, "MalformedMatchPattern", "match arm pattern head must be an identifier", ) .with_span(form.span) .expected("(some binding), (none), (ok binding), or (err binding)")); }; match head { "some" | "ok" | "err" => { if items.len() != 2 { return Err(Diagnostic::new( file, "MalformedMatchPattern", format!("`{}` match pattern requires one payload binding", head), ) .with_span(form.span) .expected(format!("({} binding)", head))); } let Some(binding) = expect_ident(&items[1]) else { return Err(Diagnostic::new( file, "MalformedMatchPattern", "match payload binding must be an identifier", ) .with_span(items[1].span) .expected("identifier")); }; let kind = match head { "some" => MatchPatternKind::Some, "ok" => MatchPatternKind::Ok, "err" => MatchPatternKind::Err, _ => unreachable!("known payload match pattern"), }; Ok(MatchPattern { kind, binding: Some(binding.to_string()), binding_span: Some(items[1].span), span: form.span, }) } "none" => { if items.len() != 1 { return Err(Diagnostic::new( file, "MalformedMatchPattern", "`none` match pattern does not take a payload binding", ) .with_span(form.span) .expected("(none)")); } Ok(MatchPattern { kind: MatchPatternKind::None, binding: None, binding_span: None, span: form.span, }) } name if qualified_enum_name(name).is_some() => { if items.len() > 2 { return Err(Diagnostic::new( file, "InvalidEnumMatchArm", "enum match patterns support at most one payload binding", ) .with_span(form.span) .expected("(Name.Variant) or (Name.Variant binding)")); } let (enum_name, variant) = qualified_enum_name(name).expect("qualified enum pattern"); let binding = if items.len() == 2 { let Some(binding) = expect_ident(&items[1]) else { return Err(Diagnostic::new( file, "MalformedMatchPattern", "match payload binding must be an identifier", ) .with_span(items[1].span) .expected("identifier")); }; Some(binding.to_string()) } else { None }; Ok(MatchPattern { kind: MatchPatternKind::EnumVariant { enum_name: enum_name.to_string(), variant: variant.to_string(), }, binding, binding_span: items.get(1).map(|item| item.span), span: form.span, }) } _ => Err(Diagnostic::new( file, "MalformedMatchPattern", format!("unsupported match pattern `{}`", head), ) .with_span(form.span) .expected("(some binding), (none), (ok binding), or (err binding)")), } } 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 starts_uppercase(name: &str) -> bool { name.chars().next().is_some_and(char::is_uppercase) } fn lower_unary_observer( file: &str, form: &SExpr, items: &[SExpr], struct_names: &HashSet, enum_names: &HashSet, name: &str, ) -> Result { if items.len() != 2 { return Err(Diagnostic::new( file, "MalformedObservationForm", format!("`{}` expects exactly one value", name), ) .with_span(form.span) .hint(format!("use `({} value)`", name))); } let value = Box::new(lower_expr(file, &items[1], struct_names, enum_names)?); let kind = match name { "is_some" => ExprKind::OptionIsSome { value }, "is_none" => ExprKind::OptionIsNone { value }, "is_ok" | "std.result.is_ok" => ExprKind::ResultIsOk { source_name: name.to_string(), value, }, "is_err" | "std.result.is_err" => ExprKind::ResultIsErr { source_name: name.to_string(), value, }, _ => unreachable!("unknown observer"), }; Ok(Expr { kind, span: form.span, }) } fn lower_unary_payload_access( file: &str, form: &SExpr, items: &[SExpr], struct_names: &HashSet, enum_names: &HashSet, name: &str, ) -> Result { if items.len() != 2 { return Err(Diagnostic::new( file, "MalformedUnwrapForm", format!("`{}` expects exactly one value", name), ) .with_span(form.span) .hint(format!("use `({} value)`", name))); } let value = Box::new(lower_expr(file, &items[1], struct_names, enum_names)?); let kind = match name { "unwrap_some" => ExprKind::OptionUnwrapSome { value }, "unwrap_ok" | "std.result.unwrap_ok" => ExprKind::ResultUnwrapOk { source_name: name.to_string(), value, }, "unwrap_err" | "std.result.unwrap_err" => ExprKind::ResultUnwrapErr { source_name: name.to_string(), value, }, _ => unreachable!("unknown payload access"), }; Ok(Expr { kind, span: form.span, }) } fn lower_unsafe( file: &str, form: &SExpr, items: &[SExpr], struct_names: &HashSet, enum_names: &HashSet, ) -> Result { if items.len() < 2 { return Err(Diagnostic::new( file, "MalformedUnsafeForm", "`unsafe` block must contain a final expression", ) .with_span(form.span) .expected("(unsafe body-form... final-expression)") .hint( "use `(unsafe expression)` or add supported body forms before the final expression", )); } let mut body = Vec::new(); for item in &items[1..] { body.push(lower_expr(file, item, struct_names, enum_names)?); } Ok(Expr { kind: ExprKind::Unsafe { body }, span: form.span, }) } fn lower_field_access( file: &str, form: &SExpr, items: &[SExpr], struct_names: &HashSet, enum_names: &HashSet, ) -> Result { if items.len() != 3 { return Err(Diagnostic::new( file, "MalformedFieldAccess", "field access must be `(. value field)`", ) .with_span(form.span)); } let Some(field) = expect_ident(&items[2]) else { return Err( Diagnostic::new(file, "InvalidFieldName", "field name must be an identifier") .with_span(items[2].span), ); }; Ok(Expr { kind: ExprKind::FieldAccess { value: Box::new(lower_expr(file, &items[1], struct_names, enum_names)?), field: field.to_string(), field_span: items[2].span, }, span: form.span, }) } fn lower_array_init( file: &str, form: &SExpr, items: &[SExpr], struct_names: &HashSet, enum_names: &HashSet, ) -> Result { if items.len() < 2 { return Err(Diagnostic::new( file, "MalformedArrayConstructor", "array constructor must be `(array TYPE value...)`", ) .with_span(form.span) .hint( "use `(array i32 1 2 3)`, `(array bool true false)`, or `(array string \"a\" \"b\")`", )); } let Some(elem_ty) = lower_type(&items[1]) else { return Err(invalid_type_diagnostic( file, &items[1], "InvalidArrayElementType", "array constructor element type is invalid", None, Some("fixed arrays use direct scalar `i32`, `i64`, `f64`, `bool`, `string`, known enum, or known non-recursive struct elements"), )); }; let mut elements = Vec::new(); for item in &items[2..] { elements.push(lower_expr(file, item, struct_names, enum_names)?); } Ok(Expr { kind: ExprKind::ArrayInit { elem_ty, elem_ty_span: items[1].span, elements, }, span: form.span, }) } fn lower_option_some( file: &str, form: &SExpr, items: &[SExpr], struct_names: &HashSet, enum_names: &HashSet, ) -> Result { if items.len() != 3 { return Err(Diagnostic::new( file, "MalformedOptionConstructor", "`some` constructor must be `(some i32 value)`", ) .with_span(form.span) .hint("use `(some i32 value)`")); } let Some(payload_ty) = lower_type(&items[1]) else { return Err(invalid_type_diagnostic( file, &items[1], "InvalidOptionPayloadType", "option constructor payload type is invalid", None, Some("first-pass options use `i32` payloads"), )); }; Ok(Expr { kind: ExprKind::OptionSome { payload_ty, payload_ty_span: items[1].span, value: Box::new(lower_expr(file, &items[2], struct_names, enum_names)?), }, span: form.span, }) } fn lower_option_none(file: &str, form: &SExpr, items: &[SExpr]) -> Result { if items.len() != 2 { return Err(Diagnostic::new( file, "MalformedOptionConstructor", "`none` constructor must be `(none i32)`", ) .with_span(form.span) .hint("use `(none i32)`")); } let Some(payload_ty) = lower_type(&items[1]) else { return Err(invalid_type_diagnostic( file, &items[1], "InvalidOptionPayloadType", "option constructor payload type is invalid", None, Some("first-pass options use `i32` payloads"), )); }; Ok(Expr { kind: ExprKind::OptionNone { payload_ty, payload_ty_span: items[1].span, }, span: form.span, }) } fn lower_result_ok( file: &str, form: &SExpr, items: &[SExpr], struct_names: &HashSet, enum_names: &HashSet, ) -> Result { if items.len() != 4 { return Err(Diagnostic::new( file, "MalformedResultConstructor", "`ok` constructor must be `(ok i32 i32 value)`", ) .with_span(form.span) .hint("use `(ok i32 i32 value)`")); } let Some(ok_ty) = lower_type(&items[1]) else { return Err(invalid_type_diagnostic( file, &items[1], "InvalidResultPayloadType", "result constructor ok type is invalid", None, Some("first-pass results use `i32` payloads"), )); }; let Some(err_ty) = lower_type(&items[2]) else { return Err(invalid_type_diagnostic( file, &items[2], "InvalidResultPayloadType", "result constructor err type is invalid", None, Some("first-pass results use `i32` payloads"), )); }; Ok(Expr { kind: ExprKind::ResultOk { ok_ty, ok_ty_span: items[1].span, err_ty, err_ty_span: items[2].span, value: Box::new(lower_expr(file, &items[3], struct_names, enum_names)?), }, span: form.span, }) } fn lower_result_err( file: &str, form: &SExpr, items: &[SExpr], struct_names: &HashSet, enum_names: &HashSet, ) -> Result { if items.len() != 4 { return Err(Diagnostic::new( file, "MalformedResultConstructor", "`err` constructor must be `(err i32 i32 value)`", ) .with_span(form.span) .hint("use `(err i32 i32 value)`")); } let Some(ok_ty) = lower_type(&items[1]) else { return Err(invalid_type_diagnostic( file, &items[1], "InvalidResultPayloadType", "result constructor ok type is invalid", None, Some("first-pass results use `i32` payloads"), )); }; let Some(err_ty) = lower_type(&items[2]) else { return Err(invalid_type_diagnostic( file, &items[2], "InvalidResultPayloadType", "result constructor err type is invalid", None, Some("first-pass results use `i32` payloads"), )); }; Ok(Expr { kind: ExprKind::ResultErr { ok_ty, ok_ty_span: items[1].span, err_ty, err_ty_span: items[2].span, value: Box::new(lower_expr(file, &items[3], struct_names, enum_names)?), }, span: form.span, }) } fn lower_index( file: &str, form: &SExpr, items: &[SExpr], struct_names: &HashSet, enum_names: &HashSet, ) -> Result { if items.len() != 3 { return Err(Diagnostic::new( file, "MalformedIndex", "index expression must be `(index array-expr index-expr)`", ) .with_span(form.span) .hint("use `(index values 0)`")); } Ok(Expr { kind: ExprKind::Index { array: Box::new(lower_expr(file, &items[1], struct_names, enum_names)?), index: Box::new(lower_expr(file, &items[2], struct_names, enum_names)?), }, span: form.span, }) } fn lower_struct_init( file: &str, form: &SExpr, items: &[SExpr], name: &str, struct_names: &HashSet, enum_names: &HashSet, ) -> Result { let mut fields = Vec::new(); for item in &items[1..] { let Some(pair) = expect_list(item) else { return Err(Diagnostic::new( file, "MalformedStructConstructor", "struct constructor fields must be `(field value)`", ) .with_span(item.span)); }; if pair.len() != 2 { return Err(Diagnostic::new( file, "MalformedStructConstructor", "struct constructor fields must be `(field value)`", ) .with_span(item.span)); } let Some(field_name) = expect_ident(&pair[0]) else { return Err(Diagnostic::new( file, "InvalidStructConstructorField", "struct constructor field name must be an identifier", ) .with_span(pair[0].span)); }; fields.push(StructInitField { name: field_name.to_string(), name_span: pair[0].span, expr: lower_expr(file, &pair[1], struct_names, enum_names)?, }); } Ok(Expr { kind: ExprKind::StructInit { name: name.to_string(), name_span: items[0].span, fields, }, span: form.span, }) } fn lower_local( file: &str, form: &SExpr, items: &[SExpr], mutable: bool, struct_names: &HashSet, enum_names: &HashSet, ) -> Result { let keyword = if mutable { "var" } else { "let" }; if items.len() != 4 { return Err(Diagnostic::new( file, "InvalidLocalDeclaration", format!("`{}` must be `({} name i32 expr)`", keyword, keyword), ) .with_span(form.span)); } let Some(name) = expect_ident(&items[1]) else { return Err( Diagnostic::new(file, "InvalidLocalName", "local name must be an identifier") .with_span(items[1].span), ); }; let Some(ty) = lower_type(&items[2]) else { return Err(invalid_type_diagnostic( file, &items[2], "InvalidLocalType", "invalid local type", None, None, )); }; Ok(Expr { kind: ExprKind::Local { mutable, name: name.to_string(), name_span: items[1].span, ty, expr: Box::new(lower_expr(file, &items[3], struct_names, enum_names)?), }, span: form.span, }) } fn lower_set( file: &str, form: &SExpr, items: &[SExpr], struct_names: &HashSet, enum_names: &HashSet, ) -> Result { if items.len() != 3 { return Err( Diagnostic::new(file, "InvalidSet", "`set` must be `(set name expr)`") .with_span(form.span), ); } let Some(name) = expect_ident(&items[1]) else { return Err(Diagnostic::new( file, "InvalidSetTarget", "`set` target must be an identifier", ) .with_span(items[1].span)); }; Ok(Expr { kind: ExprKind::Set { name: name.to_string(), name_span: items[1].span, expr: Box::new(lower_expr(file, &items[2], struct_names, enum_names)?), }, span: form.span, }) } fn lower_while( file: &str, form: &SExpr, items: &[SExpr], struct_names: &HashSet, enum_names: &HashSet, ) -> Result { if items.len() < 2 { return Err( Diagnostic::new(file, "MalformedWhileForm", "`while` must have a condition") .with_span(form.span) .hint("use `(while condition body...)`"), ); } if items.len() < 3 { return Err(Diagnostic::new( file, "EmptyWhileBody", "`while` must have at least one body form", ) .with_span(form.span) .hint("provide at least one unit-producing loop body form")); } let mut body = Vec::new(); for item in &items[2..] { body.push(lower_expr(file, item, struct_names, enum_names)?); } Ok(Expr { kind: ExprKind::While { condition: Box::new(lower_expr(file, &items[1], struct_names, enum_names)?), body, }, span: form.span, }) } fn invalid_type_diagnostic( file: &str, form: &SExpr, fallback_code: &'static str, fallback_message: impl Into, fallback_expected: Option<&str>, fallback_hint: Option<&str>, ) -> Diagnostic { if let Some(diagnostic) = unsupported_reserved_type_diagnostic(file, form) { return diagnostic; } let mut diagnostic = Diagnostic::new(file, fallback_code, fallback_message).with_span(form.span); if let Some(expected) = fallback_expected { diagnostic = diagnostic.expected(expected); } if let Some(hint) = fallback_hint { diagnostic = diagnostic.hint(hint); } diagnostic } fn list_head(form: &SExpr) -> Option<&str> { let items = expect_list(form)?; let first = items.first()?; expect_ident(first) } 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_string(form: &SExpr) -> Option<&str> { match &form.kind { SExprKind::Atom(Atom::String(value)) => Some(value.as_str()), _ => None, } } fn valid_test_name(name: &str) -> bool { !name.is_empty() && name .bytes() .all(|byte| matches!(byte, 0x20..=0x7e) && byte != b'"' && byte != b'\\') }