slovo/compiler/src/lower.rs

2577 lines
79 KiB
Rust

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<Program, Vec<Diagnostic>> {
lower_program_with_imported_names(file, forms, &[])
}
pub fn lower_program_with_imported_names(
file: &str,
forms: &[SExpr],
imported_names: &[String],
) -> Result<Program, Vec<Diagnostic>> {
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::<HashSet<_>>();
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(&param.name);
output.push_str(": ");
output.push_str(&param.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(&param.name);
output.push_str(": ");
output.push_str(&param.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<String, Diagnostic> {
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<TypeAliasDecl, Vec<Diagnostic>> {
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),
);
"<error>".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<StructDecl, Vec<Diagnostic>> {
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),
);
"<error>".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<EnumDecl, Vec<Diagnostic>> {
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),
);
"<error>".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<String>,
enum_names: &HashSet<String>,
) -> Result<Function, Vec<Diagnostic>> {
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),
);
"<error>".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<CImportDecl, Vec<Diagnostic>> {
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),
);
"<error>".to_string()
}
};
if name != "<error>" && !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 &params {
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<Param>, Vec<Diagnostic>> {
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::<String, Span>::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<String>,
enum_names: &HashSet<String>,
) -> Result<Test, Vec<Diagnostic>> {
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"),
);
"<error>".to_string()
}
None => {
errors.push(
Diagnostic::new(
file,
"InvalidTestName",
"test name must be a string literal",
)
.with_span(items[1].span)
.expected("string"),
);
"<error>".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<Param>, Vec<Diagnostic>> {
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<Type> {
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<String>,
enum_names: &HashSet<String>,
) -> Result<Expr, Diagnostic> {
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<String>,
enum_names: &HashSet<String>,
) -> Result<Expr, Diagnostic> {
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<MatchPattern, Diagnostic> {
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<String>,
enum_names: &HashSet<String>,
name: &str,
) -> Result<Expr, Diagnostic> {
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<String>,
enum_names: &HashSet<String>,
name: &str,
) -> Result<Expr, Diagnostic> {
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<String>,
enum_names: &HashSet<String>,
) -> Result<Expr, Diagnostic> {
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<String>,
enum_names: &HashSet<String>,
) -> Result<Expr, Diagnostic> {
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<String>,
enum_names: &HashSet<String>,
) -> Result<Expr, Diagnostic> {
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<String>,
enum_names: &HashSet<String>,
) -> Result<Expr, Diagnostic> {
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<Expr, Diagnostic> {
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<String>,
enum_names: &HashSet<String>,
) -> Result<Expr, Diagnostic> {
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<String>,
enum_names: &HashSet<String>,
) -> Result<Expr, Diagnostic> {
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<String>,
enum_names: &HashSet<String>,
) -> Result<Expr, Diagnostic> {
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<String>,
enum_names: &HashSet<String>,
) -> Result<Expr, Diagnostic> {
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<String>,
enum_names: &HashSet<String>,
) -> Result<Expr, Diagnostic> {
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<String>,
enum_names: &HashSet<String>,
) -> Result<Expr, Diagnostic> {
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<String>,
enum_names: &HashSet<String>,
) -> Result<Expr, Diagnostic> {
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<String>,
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'\\')
}