2577 lines
79 KiB
Rust
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(¶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<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 ¶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<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'\\')
|
|
}
|