5637 lines
181 KiB
Rust
5637 lines
181 KiB
Rust
use std::collections::{hash_map::Entry, BTreeSet, HashMap, HashSet};
|
|
|
|
use crate::{
|
|
ast::{
|
|
BinaryOp, EnumDecl, Expr, ExprKind, Function, MatchArm, MatchPatternKind, Program,
|
|
StructDecl, StructInitField, Test, TypeAliasDecl,
|
|
},
|
|
diag::Diagnostic,
|
|
lower, std_runtime,
|
|
token::Span,
|
|
types::Type,
|
|
unsafe_ops,
|
|
};
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct CheckedProgram {
|
|
pub module: String,
|
|
pub enums: Vec<CheckedEnum>,
|
|
pub structs: Vec<CheckedStruct>,
|
|
pub c_imports: Vec<CheckedCImport>,
|
|
pub functions: Vec<CheckedFunction>,
|
|
pub tests: Vec<CheckedTest>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct CheckedEnum {
|
|
pub name: String,
|
|
pub variants: Vec<CheckedEnumVariant>,
|
|
pub span: Span,
|
|
pub file: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct CheckedEnumVariant {
|
|
pub name: String,
|
|
pub payload_ty: Option<Type>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct CheckedStruct {
|
|
pub name: String,
|
|
pub fields: Vec<(String, Type)>,
|
|
pub span: Span,
|
|
pub file: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct CheckedCImport {
|
|
pub name: String,
|
|
pub params: Vec<(String, Type)>,
|
|
pub return_type: Type,
|
|
pub span: Span,
|
|
pub file: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct CheckedFunction {
|
|
pub name: String,
|
|
pub params: Vec<(String, Type)>,
|
|
pub return_type: Type,
|
|
pub body: Vec<TExpr>,
|
|
pub span: Span,
|
|
pub file: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct CheckedTest {
|
|
pub name: String,
|
|
pub name_span: Span,
|
|
pub body: Vec<TExpr>,
|
|
pub span: Span,
|
|
pub file: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct TExpr {
|
|
pub ty: Type,
|
|
pub span: Span,
|
|
pub kind: TExprKind,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum TExprKind {
|
|
Int(i32),
|
|
Int64(i64),
|
|
UInt32(u32),
|
|
UInt64(u64),
|
|
Float(f64),
|
|
Bool(bool),
|
|
String(String),
|
|
Var(String),
|
|
StructInit {
|
|
name: String,
|
|
fields: Vec<(String, TExpr)>,
|
|
},
|
|
ArrayInit {
|
|
elements: Vec<TExpr>,
|
|
},
|
|
OptionSome {
|
|
value: Box<TExpr>,
|
|
},
|
|
OptionNone,
|
|
ResultOk {
|
|
value: Box<TExpr>,
|
|
},
|
|
ResultErr {
|
|
value: Box<TExpr>,
|
|
},
|
|
OptionIsSome {
|
|
value: Box<TExpr>,
|
|
},
|
|
OptionIsNone {
|
|
value: Box<TExpr>,
|
|
},
|
|
OptionUnwrapSome {
|
|
value: Box<TExpr>,
|
|
},
|
|
ResultIsOk {
|
|
source_name: String,
|
|
value: Box<TExpr>,
|
|
},
|
|
ResultIsErr {
|
|
source_name: String,
|
|
value: Box<TExpr>,
|
|
},
|
|
ResultUnwrapOk {
|
|
source_name: String,
|
|
value: Box<TExpr>,
|
|
},
|
|
ResultUnwrapErr {
|
|
source_name: String,
|
|
value: Box<TExpr>,
|
|
},
|
|
EnumVariant {
|
|
enum_name: String,
|
|
variant: String,
|
|
discriminant: i32,
|
|
payload: Option<Box<TExpr>>,
|
|
},
|
|
FieldAccess {
|
|
value: Box<TExpr>,
|
|
field: String,
|
|
},
|
|
Index {
|
|
array: Box<TExpr>,
|
|
index: Box<TExpr>,
|
|
},
|
|
Local {
|
|
mutable: bool,
|
|
name: String,
|
|
initializer: Box<TExpr>,
|
|
},
|
|
Set {
|
|
name: String,
|
|
expr: Box<TExpr>,
|
|
},
|
|
Binary {
|
|
op: BinaryOp,
|
|
left: Box<TExpr>,
|
|
right: Box<TExpr>,
|
|
},
|
|
If {
|
|
condition: Box<TExpr>,
|
|
then_expr: Box<TExpr>,
|
|
else_expr: Box<TExpr>,
|
|
},
|
|
Match {
|
|
subject: Box<TExpr>,
|
|
arms: Vec<TMatchArm>,
|
|
},
|
|
While {
|
|
condition: Box<TExpr>,
|
|
body: Vec<TExpr>,
|
|
},
|
|
Unsafe {
|
|
body: Vec<TExpr>,
|
|
},
|
|
Call {
|
|
name: String,
|
|
args: Vec<TExpr>,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct TMatchArm {
|
|
pub pattern: MatchPatternKind,
|
|
pub binding: Option<String>,
|
|
pub discriminant: Option<i32>,
|
|
pub body: Vec<TExpr>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct FnSig {
|
|
params: Vec<Type>,
|
|
return_type: Type,
|
|
foreign: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct ExternalFunction {
|
|
pub name: String,
|
|
pub params: Vec<Type>,
|
|
pub return_type: Type,
|
|
pub foreign: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct ExternalStruct {
|
|
pub name: String,
|
|
pub fields: Vec<(String, Type)>,
|
|
pub span: Span,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct ExternalEnum {
|
|
pub name: String,
|
|
pub variants: Vec<ExternalEnumVariant>,
|
|
pub span: Span,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct ExternalEnumVariant {
|
|
pub name: String,
|
|
pub name_span: Span,
|
|
pub payload_ty: Option<Type>,
|
|
pub payload_ty_span: Option<Span>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct StructSig {
|
|
span: Span,
|
|
fields: Vec<StructFieldSig>,
|
|
fields_by_name: HashMap<String, usize>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct EnumSig {
|
|
span: Span,
|
|
variants: Vec<EnumVariantSig>,
|
|
variants_by_name: HashMap<String, usize>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct EnumVariantSig {
|
|
name: String,
|
|
name_span: Span,
|
|
payload_ty: Option<Type>,
|
|
payload_ty_span: Option<Span>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct StructFieldSig {
|
|
name: String,
|
|
name_span: Span,
|
|
ty_span: Span,
|
|
ty: Type,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct Binding {
|
|
ty: Type,
|
|
mutable: bool,
|
|
kind: BindingKind,
|
|
span: Span,
|
|
}
|
|
|
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
|
enum BindingKind {
|
|
Param,
|
|
Local,
|
|
MatchPayload,
|
|
}
|
|
|
|
pub fn check_program(file: &str, program: Program) -> Result<CheckedProgram, Vec<Diagnostic>> {
|
|
check_program_inner(file, program, &[], &[], &[], false)
|
|
}
|
|
|
|
pub fn check_program_with_imports(
|
|
file: &str,
|
|
program: Program,
|
|
external_functions: &[ExternalFunction],
|
|
external_structs: &[ExternalStruct],
|
|
external_enums: &[ExternalEnum],
|
|
) -> Result<CheckedProgram, Vec<Diagnostic>> {
|
|
check_program_inner(
|
|
file,
|
|
program,
|
|
external_functions,
|
|
external_structs,
|
|
external_enums,
|
|
true,
|
|
)
|
|
}
|
|
|
|
fn check_program_inner(
|
|
file: &str,
|
|
mut program: Program,
|
|
external_functions: &[ExternalFunction],
|
|
external_structs: &[ExternalStruct],
|
|
external_enums: &[ExternalEnum],
|
|
project_resolution: bool,
|
|
) -> Result<CheckedProgram, Vec<Diagnostic>> {
|
|
let mut errors = Vec::new();
|
|
errors.extend(resolve_type_aliases(
|
|
file,
|
|
&mut program,
|
|
external_structs,
|
|
external_enums,
|
|
));
|
|
if !errors.is_empty() {
|
|
return Err(errors);
|
|
}
|
|
|
|
let mut functions = HashMap::new();
|
|
let mut structs = HashMap::new();
|
|
let mut enums = HashMap::new();
|
|
|
|
if !project_resolution {
|
|
insert_builtin_functions(&mut functions, false);
|
|
}
|
|
|
|
for function in &program.functions {
|
|
let sig = FnSig {
|
|
params: function.params.iter().map(|p| p.ty.clone()).collect(),
|
|
return_type: function.return_type.clone(),
|
|
foreign: false,
|
|
};
|
|
|
|
if is_reserved_callable_name(&function.name) && !functions.contains_key(&function.name) {
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"DuplicateFunction",
|
|
format!("compiler-known callable `{}` is reserved", function.name),
|
|
)
|
|
.with_span(function.span),
|
|
);
|
|
}
|
|
|
|
if functions.insert(function.name.clone(), sig).is_some() {
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"DuplicateFunction",
|
|
format!("duplicate function `{}`", function.name),
|
|
)
|
|
.with_span(function.span),
|
|
);
|
|
}
|
|
}
|
|
|
|
for function in external_functions {
|
|
functions.insert(
|
|
function.name.clone(),
|
|
FnSig {
|
|
params: function.params.clone(),
|
|
return_type: function.return_type.clone(),
|
|
foreign: function.foreign,
|
|
},
|
|
);
|
|
}
|
|
|
|
for import in &program.c_imports {
|
|
if is_reserved_callable_name(&import.name) {
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"ReservedName",
|
|
format!("C import name `{}` is reserved", import.name),
|
|
)
|
|
.with_span(import.name_span),
|
|
);
|
|
continue;
|
|
}
|
|
|
|
let sig = FnSig {
|
|
params: import.params.iter().map(|p| p.ty.clone()).collect(),
|
|
return_type: import.return_type.clone(),
|
|
foreign: true,
|
|
};
|
|
if functions.insert(import.name.clone(), sig).is_some() {
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"DuplicateTopLevelName",
|
|
format!("duplicate top-level callable `{}`", import.name),
|
|
)
|
|
.with_span(import.name_span),
|
|
);
|
|
}
|
|
}
|
|
|
|
if project_resolution {
|
|
insert_builtin_functions(&mut functions, true);
|
|
}
|
|
|
|
let mut declared_struct_names = HashMap::new();
|
|
for struct_decl in &program.structs {
|
|
declared_struct_names
|
|
.entry(struct_decl.name.clone())
|
|
.or_insert(struct_decl.name_span);
|
|
}
|
|
for struct_decl in external_structs {
|
|
declared_struct_names
|
|
.entry(struct_decl.name.clone())
|
|
.or_insert(struct_decl.span);
|
|
}
|
|
|
|
let mut checked_structs = Vec::new();
|
|
|
|
let mut checked_enums = Vec::new();
|
|
|
|
for enum_decl in &program.enums {
|
|
match check_enum_decl(file, enum_decl, &functions, &declared_struct_names) {
|
|
Ok((checked, sig)) => match enums.entry(enum_decl.name.clone()) {
|
|
Entry::Vacant(entry) => {
|
|
entry.insert(sig);
|
|
checked_enums.push(checked);
|
|
}
|
|
Entry::Occupied(entry) => errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"DuplicateEnum",
|
|
format!("duplicate enum `{}`", enum_decl.name),
|
|
)
|
|
.with_span(enum_decl.name_span)
|
|
.related("original enum declaration", entry.get().span),
|
|
),
|
|
},
|
|
Err(mut errs) => errors.append(&mut errs),
|
|
}
|
|
}
|
|
|
|
for enum_decl in external_enums {
|
|
enums.insert(enum_decl.name.clone(), external_enum_sig(enum_decl));
|
|
}
|
|
|
|
for struct_decl in &program.structs {
|
|
if let Some(import) = program
|
|
.c_imports
|
|
.iter()
|
|
.find(|import| import.name == struct_decl.name)
|
|
{
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"DuplicateTopLevelName",
|
|
format!("C import `{}` conflicts with a struct", import.name),
|
|
)
|
|
.with_span(import.name_span)
|
|
.related("struct declaration", struct_decl.name_span),
|
|
);
|
|
}
|
|
match check_struct_decl(
|
|
file,
|
|
struct_decl,
|
|
&functions,
|
|
&declared_struct_names,
|
|
&enums,
|
|
) {
|
|
Ok((checked, sig)) => match structs.entry(struct_decl.name.clone()) {
|
|
Entry::Vacant(entry) => {
|
|
entry.insert(sig);
|
|
checked_structs.push(checked);
|
|
}
|
|
Entry::Occupied(entry) => errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"DuplicateStruct",
|
|
format!("duplicate struct `{}`", struct_decl.name),
|
|
)
|
|
.with_span(struct_decl.name_span)
|
|
.related("original struct declaration", entry.get().span),
|
|
),
|
|
},
|
|
Err(mut errs) => errors.append(&mut errs),
|
|
}
|
|
}
|
|
|
|
for struct_decl in external_structs {
|
|
let mut fields = Vec::new();
|
|
let mut fields_by_name = HashMap::new();
|
|
for (index, (name, ty)) in struct_decl.fields.iter().enumerate() {
|
|
fields_by_name.insert(name.clone(), index);
|
|
fields.push(StructFieldSig {
|
|
name: name.clone(),
|
|
name_span: struct_decl.span,
|
|
ty_span: struct_decl.span,
|
|
ty: ty.clone(),
|
|
});
|
|
}
|
|
structs.insert(
|
|
struct_decl.name.clone(),
|
|
StructSig {
|
|
span: struct_decl.span,
|
|
fields,
|
|
fields_by_name,
|
|
},
|
|
);
|
|
}
|
|
|
|
errors.extend(validate_struct_field_cycles(file, &structs));
|
|
errors.extend(validate_enum_payload_struct_cycles(file, &enums, &structs));
|
|
|
|
let mut checked_functions = Vec::new();
|
|
for enum_decl in &program.enums {
|
|
if let Some(import) = program
|
|
.c_imports
|
|
.iter()
|
|
.find(|import| import.name == enum_decl.name)
|
|
{
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"DuplicateTopLevelName",
|
|
format!("C import `{}` conflicts with an enum", import.name),
|
|
)
|
|
.with_span(import.name_span)
|
|
.related("enum declaration", enum_decl.name_span),
|
|
);
|
|
}
|
|
}
|
|
let checked_c_imports = program
|
|
.c_imports
|
|
.iter()
|
|
.map(|import| CheckedCImport {
|
|
name: import.name.clone(),
|
|
params: import
|
|
.params
|
|
.iter()
|
|
.map(|param| (param.name.clone(), param.ty.clone()))
|
|
.collect(),
|
|
return_type: import.return_type.clone(),
|
|
span: import.span,
|
|
file: file.to_string(),
|
|
})
|
|
.collect();
|
|
|
|
for function in program.functions {
|
|
match check_function(file, function, &functions, &structs, &enums) {
|
|
Ok(function) => checked_functions.push(function),
|
|
Err(mut errs) => errors.append(&mut errs),
|
|
}
|
|
}
|
|
|
|
let mut checked_tests = Vec::new();
|
|
|
|
for test in program.tests {
|
|
match check_test(file, test, &functions, &structs, &enums) {
|
|
Ok(test) => checked_tests.push(test),
|
|
Err(mut errs) => errors.append(&mut errs),
|
|
}
|
|
}
|
|
|
|
if errors.is_empty() {
|
|
Ok(CheckedProgram {
|
|
module: program.module,
|
|
enums: checked_enums,
|
|
structs: checked_structs,
|
|
c_imports: checked_c_imports,
|
|
functions: checked_functions,
|
|
tests: checked_tests,
|
|
})
|
|
} else {
|
|
Err(errors)
|
|
}
|
|
}
|
|
|
|
fn insert_builtin_functions(functions: &mut HashMap<String, FnSig>, keep_existing: bool) {
|
|
for function in std_runtime::FUNCTIONS {
|
|
if keep_existing && functions.contains_key(function.source_name) {
|
|
continue;
|
|
}
|
|
functions.insert(
|
|
function.source_name.to_string(),
|
|
FnSig {
|
|
params: function
|
|
.params
|
|
.iter()
|
|
.map(|param| param.to_type())
|
|
.collect(),
|
|
return_type: function.return_type.to_type(),
|
|
foreign: false,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
fn is_reserved_callable_name(name: &str) -> bool {
|
|
std_runtime::is_reserved_name(name) || unsafe_ops::is_reserved_head(name)
|
|
}
|
|
|
|
fn resolve_type_aliases(
|
|
file: &str,
|
|
program: &mut Program,
|
|
external_structs: &[ExternalStruct],
|
|
external_enums: &[ExternalEnum],
|
|
) -> Vec<Diagnostic> {
|
|
if program.type_aliases.is_empty() {
|
|
return Vec::new();
|
|
}
|
|
|
|
let mut errors = Vec::new();
|
|
let mut struct_names = HashMap::new();
|
|
for struct_decl in &program.structs {
|
|
struct_names
|
|
.entry(struct_decl.name.clone())
|
|
.or_insert(struct_decl.name_span);
|
|
}
|
|
for struct_decl in external_structs {
|
|
struct_names
|
|
.entry(struct_decl.name.clone())
|
|
.or_insert(struct_decl.span);
|
|
}
|
|
|
|
let mut enum_names = HashMap::new();
|
|
for enum_decl in &program.enums {
|
|
enum_names
|
|
.entry(enum_decl.name.clone())
|
|
.or_insert(enum_decl.name_span);
|
|
}
|
|
for enum_decl in external_enums {
|
|
enum_names
|
|
.entry(enum_decl.name.clone())
|
|
.or_insert(enum_decl.span);
|
|
}
|
|
|
|
let mut function_names = HashMap::new();
|
|
for function in &program.functions {
|
|
function_names
|
|
.entry(function.name.clone())
|
|
.or_insert(function.span);
|
|
}
|
|
let mut c_import_names = HashMap::new();
|
|
for import in &program.c_imports {
|
|
c_import_names
|
|
.entry(import.name.clone())
|
|
.or_insert(import.name_span);
|
|
}
|
|
|
|
let mut aliases = HashMap::<String, &TypeAliasDecl>::new();
|
|
for alias in &program.type_aliases {
|
|
match aliases.entry(alias.name.clone()) {
|
|
Entry::Vacant(entry) => {
|
|
entry.insert(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().name_span),
|
|
),
|
|
}
|
|
}
|
|
|
|
for alias in &program.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(span) = struct_names.get(&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", *span),
|
|
);
|
|
}
|
|
if let Some(span) = enum_names.get(&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", *span),
|
|
);
|
|
}
|
|
if let Some(span) = function_names.get(&alias.name) {
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"TypeAliasNameConflict",
|
|
format!("type alias `{}` conflicts with a function", alias.name),
|
|
)
|
|
.with_span(alias.name_span)
|
|
.related("function declaration", *span),
|
|
);
|
|
}
|
|
if let Some(span) = c_import_names.get(&alias.name) {
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"TypeAliasNameConflict",
|
|
format!("type alias `{}` conflicts with a C import", alias.name),
|
|
)
|
|
.with_span(alias.name_span)
|
|
.related("C import declaration", *span),
|
|
);
|
|
}
|
|
if matches!(&alias.target, Type::Named(name) if name == &alias.name) {
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"SelfTypeAlias",
|
|
format!("type alias `{}` directly aliases itself", alias.name),
|
|
)
|
|
.with_span(alias.target_span)
|
|
.hint("point the alias at an existing concrete type"),
|
|
);
|
|
}
|
|
}
|
|
|
|
if !errors.is_empty() {
|
|
return errors;
|
|
}
|
|
|
|
let known_structs = struct_names.keys().cloned().collect::<HashSet<_>>();
|
|
let known_enums = enum_names.keys().cloned().collect::<HashSet<_>>();
|
|
let mut resolved = HashMap::new();
|
|
let mut failed = HashSet::new();
|
|
let mut reported_cycles = BTreeSet::new();
|
|
let mut names = aliases.keys().cloned().collect::<Vec<_>>();
|
|
names.sort();
|
|
for name in names {
|
|
resolve_alias_target(
|
|
file,
|
|
&name,
|
|
&aliases,
|
|
&known_structs,
|
|
&known_enums,
|
|
&mut resolved,
|
|
&mut failed,
|
|
&mut Vec::new(),
|
|
&mut reported_cycles,
|
|
&mut errors,
|
|
);
|
|
}
|
|
|
|
if !errors.is_empty() {
|
|
return errors;
|
|
}
|
|
|
|
rewrite_program_alias_types(program, &resolved);
|
|
Vec::new()
|
|
}
|
|
|
|
fn resolve_alias_target(
|
|
file: &str,
|
|
name: &str,
|
|
aliases: &HashMap<String, &TypeAliasDecl>,
|
|
structs: &HashSet<String>,
|
|
enums: &HashSet<String>,
|
|
resolved: &mut HashMap<String, Type>,
|
|
failed: &mut HashSet<String>,
|
|
stack: &mut Vec<String>,
|
|
reported_cycles: &mut BTreeSet<String>,
|
|
errors: &mut Vec<Diagnostic>,
|
|
) -> Option<Type> {
|
|
if let Some(ty) = resolved.get(name) {
|
|
return Some(ty.clone());
|
|
}
|
|
if failed.contains(name) {
|
|
return None;
|
|
}
|
|
if let Some(cycle_start) = stack.iter().position(|candidate| candidate == name) {
|
|
let cycle = stack[cycle_start..].to_vec();
|
|
let mut key = cycle.clone();
|
|
key.sort();
|
|
if reported_cycles.insert(key.join("|")) {
|
|
errors.push(type_alias_cycle(file, &cycle, aliases));
|
|
}
|
|
failed.extend(cycle);
|
|
return None;
|
|
}
|
|
|
|
let alias = aliases.get(name).copied()?;
|
|
stack.push(name.to_string());
|
|
let target = resolve_alias_target_type(
|
|
file,
|
|
&alias.target,
|
|
alias.target_span,
|
|
aliases,
|
|
structs,
|
|
enums,
|
|
resolved,
|
|
failed,
|
|
stack,
|
|
reported_cycles,
|
|
errors,
|
|
);
|
|
stack.pop();
|
|
|
|
if let Some(target) = target {
|
|
resolved.insert(name.to_string(), target.clone());
|
|
Some(target)
|
|
} else {
|
|
failed.insert(name.to_string());
|
|
None
|
|
}
|
|
}
|
|
|
|
fn resolve_alias_target_type(
|
|
file: &str,
|
|
ty: &Type,
|
|
span: Span,
|
|
aliases: &HashMap<String, &TypeAliasDecl>,
|
|
structs: &HashSet<String>,
|
|
enums: &HashSet<String>,
|
|
resolved: &mut HashMap<String, Type>,
|
|
failed: &mut HashSet<String>,
|
|
stack: &mut Vec<String>,
|
|
reported_cycles: &mut BTreeSet<String>,
|
|
errors: &mut Vec<Diagnostic>,
|
|
) -> Option<Type> {
|
|
match ty {
|
|
Type::Named(name) if aliases.contains_key(name) => resolve_alias_target(
|
|
file,
|
|
name,
|
|
aliases,
|
|
structs,
|
|
enums,
|
|
resolved,
|
|
failed,
|
|
stack,
|
|
reported_cycles,
|
|
errors,
|
|
),
|
|
Type::Named(name) if structs.contains(name) || enums.contains(name) => {
|
|
Some(Type::Named(name.clone()))
|
|
}
|
|
Type::Named(name) => {
|
|
if is_generic_type_parameter_name(name) {
|
|
errors.push(unsupported_generic_type_parameter(file, span, name));
|
|
return None;
|
|
}
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"UnknownTypeAliasTarget",
|
|
format!("type alias target `{}` is not a known concrete type", name),
|
|
)
|
|
.with_span(span)
|
|
.expected("built-in type, known struct, known enum, or known type alias")
|
|
.found(name.clone())
|
|
.hint("declare the target type or alias before checking this alias set"),
|
|
);
|
|
None
|
|
}
|
|
Type::Array(inner, len) => {
|
|
let resolved_inner = resolve_alias_target_type(
|
|
file,
|
|
inner,
|
|
span,
|
|
aliases,
|
|
structs,
|
|
enums,
|
|
resolved,
|
|
failed,
|
|
stack,
|
|
reported_cycles,
|
|
errors,
|
|
)?;
|
|
let target = Type::Array(Box::new(resolved_inner), *len);
|
|
if alias_target_type_supported(&target, structs, enums) {
|
|
Some(target)
|
|
} else {
|
|
errors.push(unsupported_type_alias_target(file, span, &target));
|
|
None
|
|
}
|
|
}
|
|
Type::Vec(inner) => {
|
|
let resolved_inner = resolve_alias_target_type(
|
|
file,
|
|
inner,
|
|
span,
|
|
aliases,
|
|
structs,
|
|
enums,
|
|
resolved,
|
|
failed,
|
|
stack,
|
|
reported_cycles,
|
|
errors,
|
|
)?;
|
|
let target = Type::Vec(Box::new(resolved_inner));
|
|
if alias_target_type_supported(&target, structs, enums) {
|
|
Some(target)
|
|
} else {
|
|
errors.push(unsupported_type_alias_target(file, span, &target));
|
|
None
|
|
}
|
|
}
|
|
Type::Option(inner) => {
|
|
let resolved_inner = resolve_alias_target_type(
|
|
file,
|
|
inner,
|
|
span,
|
|
aliases,
|
|
structs,
|
|
enums,
|
|
resolved,
|
|
failed,
|
|
stack,
|
|
reported_cycles,
|
|
errors,
|
|
)?;
|
|
let target = Type::Option(Box::new(resolved_inner));
|
|
if alias_target_type_supported(&target, structs, enums) {
|
|
Some(target)
|
|
} else {
|
|
errors.push(unsupported_type_alias_target(file, span, &target));
|
|
None
|
|
}
|
|
}
|
|
Type::Result(ok, err) => {
|
|
let resolved_ok = resolve_alias_target_type(
|
|
file,
|
|
ok,
|
|
span,
|
|
aliases,
|
|
structs,
|
|
enums,
|
|
resolved,
|
|
failed,
|
|
stack,
|
|
reported_cycles,
|
|
errors,
|
|
)?;
|
|
let resolved_err = resolve_alias_target_type(
|
|
file,
|
|
err,
|
|
span,
|
|
aliases,
|
|
structs,
|
|
enums,
|
|
resolved,
|
|
failed,
|
|
stack,
|
|
reported_cycles,
|
|
errors,
|
|
)?;
|
|
let target = Type::Result(Box::new(resolved_ok), Box::new(resolved_err));
|
|
if alias_target_type_supported(&target, structs, enums) {
|
|
Some(target)
|
|
} else {
|
|
errors.push(unsupported_type_alias_target(file, span, &target));
|
|
None
|
|
}
|
|
}
|
|
_ => {
|
|
if alias_target_type_supported(ty, structs, enums) {
|
|
Some(ty.clone())
|
|
} else {
|
|
errors.push(unsupported_type_alias_target(file, span, ty));
|
|
None
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn alias_target_type_supported(
|
|
ty: &Type,
|
|
structs: &HashSet<String>,
|
|
enums: &HashSet<String>,
|
|
) -> bool {
|
|
match ty {
|
|
Type::I32 | Type::I64 | Type::U32 | Type::U64 | Type::F64 | Type::Bool | Type::String => {
|
|
true
|
|
}
|
|
Type::Named(name) => structs.contains(name) || enums.contains(name),
|
|
Type::Array(inner, len) => {
|
|
*len > 0
|
|
&& (matches!(
|
|
&**inner,
|
|
Type::I32
|
|
| Type::I64
|
|
| Type::U32
|
|
| Type::U64
|
|
| Type::F64
|
|
| Type::Bool
|
|
| Type::String
|
|
) || matches!(&**inner, Type::Named(name) if structs.contains(name) || enums.contains(name)))
|
|
}
|
|
Type::Vec(inner) => matches!(
|
|
&**inner,
|
|
Type::I32 | Type::I64 | Type::F64 | Type::Bool | Type::String
|
|
),
|
|
Type::Option(inner) => option_payload_type_supported(inner),
|
|
Type::Result(ok, err) => result_payload_types_supported(ok, err),
|
|
Type::Unit | Type::Ptr(_) | Type::Slice(_) => false,
|
|
}
|
|
}
|
|
|
|
fn unsupported_type_alias_target(file: &str, span: Span, ty: &Type) -> Diagnostic {
|
|
Diagnostic::new(
|
|
file,
|
|
"UnsupportedTypeAliasTarget",
|
|
"type alias target type is not supported in the current beta",
|
|
)
|
|
.with_span(span)
|
|
.expected("i32, i64, u32, u64, f64, bool, string, known struct, known enum, supported array, supported option, supported result, or supported vec")
|
|
.found(ty.to_string())
|
|
.hint("aliases are transparent and may only target concrete types already supported in the target use positions")
|
|
}
|
|
|
|
fn type_alias_cycle(
|
|
file: &str,
|
|
cycle: &[String],
|
|
aliases: &HashMap<String, &TypeAliasDecl>,
|
|
) -> Diagnostic {
|
|
let first = &cycle[0];
|
|
let first_alias = aliases[first];
|
|
let mut diag = Diagnostic::new(
|
|
file,
|
|
"TypeAliasCycle",
|
|
format!("type alias cycle includes `{}`", first),
|
|
)
|
|
.with_span(first_alias.name_span)
|
|
.hint("type aliases must resolve to an existing non-alias concrete type");
|
|
|
|
for name in &cycle[1..] {
|
|
diag = diag.related(
|
|
format!("cycle also includes `{}`", name),
|
|
aliases[name].name_span,
|
|
);
|
|
}
|
|
|
|
diag
|
|
}
|
|
|
|
fn rewrite_program_alias_types(program: &mut Program, aliases: &HashMap<String, Type>) {
|
|
for enum_decl in &mut program.enums {
|
|
for variant in &mut enum_decl.variants {
|
|
if let Some(payload_ty) = &mut variant.payload_ty {
|
|
*payload_ty = resolve_use_type(payload_ty, aliases);
|
|
}
|
|
}
|
|
}
|
|
|
|
for struct_decl in &mut program.structs {
|
|
for field in &mut struct_decl.fields {
|
|
field.ty = resolve_use_type(&field.ty, aliases);
|
|
}
|
|
}
|
|
|
|
for import in &mut program.c_imports {
|
|
for param in &mut import.params {
|
|
param.ty = resolve_use_type(¶m.ty, aliases);
|
|
}
|
|
import.return_type = resolve_use_type(&import.return_type, aliases);
|
|
}
|
|
|
|
for function in &mut program.functions {
|
|
for param in &mut function.params {
|
|
param.ty = resolve_use_type(¶m.ty, aliases);
|
|
}
|
|
function.return_type = resolve_use_type(&function.return_type, aliases);
|
|
for expr in &mut function.body {
|
|
rewrite_expr_alias_types(expr, aliases);
|
|
}
|
|
}
|
|
|
|
for test in &mut program.tests {
|
|
for expr in &mut test.body {
|
|
rewrite_expr_alias_types(expr, aliases);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn rewrite_expr_alias_types(expr: &mut Expr, aliases: &HashMap<String, Type>) {
|
|
match &mut expr.kind {
|
|
ExprKind::StructInit { fields, .. } => {
|
|
for field in fields {
|
|
rewrite_expr_alias_types(&mut field.expr, aliases);
|
|
}
|
|
}
|
|
ExprKind::ArrayInit {
|
|
elem_ty, elements, ..
|
|
} => {
|
|
*elem_ty = resolve_use_type(elem_ty, aliases);
|
|
for element in elements {
|
|
rewrite_expr_alias_types(element, aliases);
|
|
}
|
|
}
|
|
ExprKind::OptionSome {
|
|
payload_ty, value, ..
|
|
} => {
|
|
*payload_ty = resolve_use_type(payload_ty, aliases);
|
|
rewrite_expr_alias_types(value, aliases);
|
|
}
|
|
ExprKind::OptionNone { payload_ty, .. } => {
|
|
*payload_ty = resolve_use_type(payload_ty, aliases);
|
|
}
|
|
ExprKind::ResultOk {
|
|
ok_ty,
|
|
err_ty,
|
|
value,
|
|
..
|
|
}
|
|
| ExprKind::ResultErr {
|
|
ok_ty,
|
|
err_ty,
|
|
value,
|
|
..
|
|
} => {
|
|
*ok_ty = resolve_use_type(ok_ty, aliases);
|
|
*err_ty = resolve_use_type(err_ty, aliases);
|
|
rewrite_expr_alias_types(value, aliases);
|
|
}
|
|
ExprKind::OptionIsSome { value }
|
|
| ExprKind::OptionIsNone { value }
|
|
| ExprKind::OptionUnwrapSome { value }
|
|
| ExprKind::ResultIsOk { value, .. }
|
|
| ExprKind::ResultIsErr { value, .. }
|
|
| ExprKind::ResultUnwrapOk { value, .. }
|
|
| ExprKind::ResultUnwrapErr { value, .. } => rewrite_expr_alias_types(value, aliases),
|
|
ExprKind::EnumVariant { args, .. } | ExprKind::Call { args, .. } => {
|
|
for arg in args {
|
|
rewrite_expr_alias_types(arg, aliases);
|
|
}
|
|
}
|
|
ExprKind::FieldAccess { value, .. } => rewrite_expr_alias_types(value, aliases),
|
|
ExprKind::Index { array, index } => {
|
|
rewrite_expr_alias_types(array, aliases);
|
|
rewrite_expr_alias_types(index, aliases);
|
|
}
|
|
ExprKind::Local { ty, expr, .. } => {
|
|
*ty = resolve_use_type(ty, aliases);
|
|
rewrite_expr_alias_types(expr, aliases);
|
|
}
|
|
ExprKind::Set { expr, .. } => rewrite_expr_alias_types(expr, aliases),
|
|
ExprKind::Binary { left, right, .. } => {
|
|
rewrite_expr_alias_types(left, aliases);
|
|
rewrite_expr_alias_types(right, aliases);
|
|
}
|
|
ExprKind::If {
|
|
condition,
|
|
then_expr,
|
|
else_expr,
|
|
} => {
|
|
rewrite_expr_alias_types(condition, aliases);
|
|
rewrite_expr_alias_types(then_expr, aliases);
|
|
rewrite_expr_alias_types(else_expr, aliases);
|
|
}
|
|
ExprKind::Match { subject, arms } => {
|
|
rewrite_expr_alias_types(subject, aliases);
|
|
for arm in arms {
|
|
for expr in &mut arm.body {
|
|
rewrite_expr_alias_types(expr, aliases);
|
|
}
|
|
}
|
|
}
|
|
ExprKind::While { condition, body } => {
|
|
rewrite_expr_alias_types(condition, aliases);
|
|
for expr in body {
|
|
rewrite_expr_alias_types(expr, aliases);
|
|
}
|
|
}
|
|
ExprKind::Unsafe { body } => {
|
|
for expr in body {
|
|
rewrite_expr_alias_types(expr, aliases);
|
|
}
|
|
}
|
|
ExprKind::Int(_)
|
|
| ExprKind::Int64(_)
|
|
| ExprKind::UInt32(_)
|
|
| ExprKind::UInt64(_)
|
|
| ExprKind::Float(_)
|
|
| ExprKind::Bool(_)
|
|
| ExprKind::String(_)
|
|
| ExprKind::Var(_) => {}
|
|
}
|
|
}
|
|
|
|
fn resolve_use_type(ty: &Type, aliases: &HashMap<String, Type>) -> Type {
|
|
match ty {
|
|
Type::Named(name) => aliases
|
|
.get(name)
|
|
.cloned()
|
|
.unwrap_or_else(|| Type::Named(name.clone())),
|
|
Type::Ptr(inner) => Type::Ptr(Box::new(resolve_use_type(inner, aliases))),
|
|
Type::Array(inner, len) => Type::Array(Box::new(resolve_use_type(inner, aliases)), *len),
|
|
Type::Vec(inner) => Type::Vec(Box::new(resolve_use_type(inner, aliases))),
|
|
Type::Slice(inner) => Type::Slice(Box::new(resolve_use_type(inner, aliases))),
|
|
Type::Option(inner) => Type::Option(Box::new(resolve_use_type(inner, aliases))),
|
|
Type::Result(ok, err) => Type::Result(
|
|
Box::new(resolve_use_type(ok, aliases)),
|
|
Box::new(resolve_use_type(err, aliases)),
|
|
),
|
|
_ => ty.clone(),
|
|
}
|
|
}
|
|
|
|
fn is_reserved_type_name(name: &str) -> bool {
|
|
matches!(
|
|
name,
|
|
"i32"
|
|
| "i64"
|
|
| "u32"
|
|
| "u64"
|
|
| "f64"
|
|
| "bool"
|
|
| "unit"
|
|
| "string"
|
|
| "ptr"
|
|
| "slice"
|
|
| "option"
|
|
| "result"
|
|
| "array"
|
|
| "vec"
|
|
)
|
|
}
|
|
|
|
fn check_struct_decl(
|
|
file: &str,
|
|
struct_decl: &StructDecl,
|
|
functions: &HashMap<String, FnSig>,
|
|
declared_struct_names: &HashMap<String, Span>,
|
|
enums: &HashMap<String, EnumSig>,
|
|
) -> Result<(CheckedStruct, StructSig), Vec<Diagnostic>> {
|
|
let mut errors = Vec::new();
|
|
let mut fields: Vec<StructFieldSig> = Vec::new();
|
|
let mut fields_by_name = HashMap::new();
|
|
|
|
if functions.contains_key(&struct_decl.name) || is_reserved_callable_name(&struct_decl.name) {
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"StructNameConflictsCallable",
|
|
format!(
|
|
"struct `{}` conflicts with a function or compiler intrinsic",
|
|
struct_decl.name
|
|
),
|
|
)
|
|
.with_span(struct_decl.name_span)
|
|
.hint("choose a struct name distinct from functions and compiler intrinsics"),
|
|
);
|
|
}
|
|
|
|
if struct_decl.fields.is_empty() {
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"EmptyStructUnsupported",
|
|
"first-pass structs must declare at least one field",
|
|
)
|
|
.with_span(struct_decl.span)
|
|
.hint("declare at least one `i32` field"),
|
|
);
|
|
}
|
|
|
|
for field in &struct_decl.fields {
|
|
match fields_by_name.entry(field.name.clone()) {
|
|
Entry::Vacant(entry) => {
|
|
let index = fields.len();
|
|
entry.insert(index);
|
|
}
|
|
Entry::Occupied(entry) => {
|
|
let original = &fields[*entry.get()];
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"DuplicateStructField",
|
|
format!(
|
|
"duplicate field `{}` in struct `{}`",
|
|
field.name, struct_decl.name
|
|
),
|
|
)
|
|
.with_span(field.name_span)
|
|
.related("original field declaration", original.name_span),
|
|
);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
let direct_primitive_field = matches!(
|
|
field.ty,
|
|
Type::I32 | Type::I64 | Type::U32 | Type::U64 | Type::F64 | Type::Bool | Type::String
|
|
);
|
|
let direct_enum_field = is_known_enum_type(&field.ty, enums);
|
|
let direct_struct_field = matches!(
|
|
&field.ty,
|
|
Type::Named(name) if declared_struct_names.contains_key(name)
|
|
);
|
|
if contains_enum_type(&field.ty, enums) && !direct_enum_field && !is_array_type(&field.ty) {
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"UnsupportedEnumContainer",
|
|
"enum values in struct field containers are not supported",
|
|
)
|
|
.with_span(field.ty_span)
|
|
.hint("store enum values directly as struct fields"),
|
|
);
|
|
} else if is_array_type(&field.ty) {
|
|
if let Err(err) =
|
|
check_array_type_decl(file, &field.ty, field.ty_span, declared_struct_names, enums)
|
|
{
|
|
errors.push(err);
|
|
}
|
|
} else if is_vector_type(&field.ty) {
|
|
if let Err(err) = check_vector_type(file, &field.ty, field.ty_span) {
|
|
errors.push(err);
|
|
}
|
|
} else if is_option_result_type(&field.ty) {
|
|
if let Err(err) = check_option_result_type(file, &field.ty, field.ty_span) {
|
|
errors.push(err);
|
|
}
|
|
} else if !direct_primitive_field && !direct_enum_field && !direct_struct_field {
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"UnsupportedStructFieldType",
|
|
supported_struct_field_message(),
|
|
)
|
|
.with_span(field.ty_span)
|
|
.expected(supported_struct_field_expected())
|
|
.found(field.ty.to_string()),
|
|
);
|
|
}
|
|
|
|
fields.push(StructFieldSig {
|
|
name: field.name.clone(),
|
|
name_span: field.name_span,
|
|
ty_span: field.ty_span,
|
|
ty: field.ty.clone(),
|
|
});
|
|
}
|
|
|
|
if errors.is_empty() {
|
|
Ok((
|
|
CheckedStruct {
|
|
name: struct_decl.name.clone(),
|
|
fields: fields
|
|
.iter()
|
|
.map(|field| (field.name.clone(), field.ty.clone()))
|
|
.collect(),
|
|
span: struct_decl.span,
|
|
file: file.to_string(),
|
|
},
|
|
StructSig {
|
|
span: struct_decl.name_span,
|
|
fields,
|
|
fields_by_name,
|
|
},
|
|
))
|
|
} else {
|
|
Err(errors)
|
|
}
|
|
}
|
|
|
|
fn check_enum_decl(
|
|
file: &str,
|
|
enum_decl: &EnumDecl,
|
|
functions: &HashMap<String, FnSig>,
|
|
declared_struct_names: &HashMap<String, Span>,
|
|
) -> Result<(CheckedEnum, EnumSig), Vec<Diagnostic>> {
|
|
let mut errors = Vec::new();
|
|
let mut variants: Vec<EnumVariantSig> = Vec::new();
|
|
let mut variants_by_name = HashMap::new();
|
|
|
|
if functions.contains_key(&enum_decl.name) || is_reserved_callable_name(&enum_decl.name) {
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"EnumNameConflictsCallable",
|
|
format!(
|
|
"enum `{}` conflicts with a function or compiler intrinsic",
|
|
enum_decl.name
|
|
),
|
|
)
|
|
.with_span(enum_decl.name_span)
|
|
.hint("choose an enum name distinct from functions and compiler intrinsics"),
|
|
);
|
|
}
|
|
|
|
if enum_decl.variants.is_empty() {
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"EmptyEnumUnsupported",
|
|
"enum declarations must contain at least one variant",
|
|
)
|
|
.with_span(enum_decl.span)
|
|
.hint("declare at least one payloadless or unary direct scalar/string payload variant"),
|
|
);
|
|
}
|
|
|
|
let mut enum_payload_ty: Option<(Type, Span)> = None;
|
|
for variant in &enum_decl.variants {
|
|
match variants_by_name.entry(variant.name.clone()) {
|
|
Entry::Vacant(entry) => {
|
|
entry.insert(variants.len());
|
|
}
|
|
Entry::Occupied(entry) => {
|
|
let original = &variants[*entry.get()];
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"DuplicateEnumVariant",
|
|
format!(
|
|
"duplicate variant `{}` in enum `{}`",
|
|
variant.name, enum_decl.name
|
|
),
|
|
)
|
|
.with_span(variant.name_span)
|
|
.related("original variant declaration", original.name_span),
|
|
);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if let Some(payload_ty) = &variant.payload_ty {
|
|
if !enum_payload_type_supported(payload_ty, declared_struct_names) {
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"UnsupportedEnumPayloadType",
|
|
supported_enum_payload_message(),
|
|
)
|
|
.with_span(variant.payload_ty_span.unwrap_or(variant.name_span))
|
|
.expected(supported_enum_payload_expected())
|
|
.found(payload_ty.to_string())
|
|
.hint("use a direct scalar/string payload, a known non-recursive struct payload, or keep the variant payloadless"),
|
|
);
|
|
} else if let Some((expected_ty, expected_span)) = &enum_payload_ty {
|
|
if payload_ty != expected_ty {
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"MixedEnumPayloadTypesUnsupported",
|
|
"enum payload variants in one enum must use the same payload type",
|
|
)
|
|
.with_span(variant.payload_ty_span.unwrap_or(variant.name_span))
|
|
.related("first payload type in this enum", *expected_span)
|
|
.expected(expected_ty.to_string())
|
|
.found(payload_ty.to_string())
|
|
.hint("split mixed payload kinds into separate enums"),
|
|
);
|
|
}
|
|
} else {
|
|
enum_payload_ty = Some((
|
|
payload_ty.clone(),
|
|
variant.payload_ty_span.unwrap_or(variant.name_span),
|
|
));
|
|
}
|
|
}
|
|
|
|
variants.push(EnumVariantSig {
|
|
name: variant.name.clone(),
|
|
name_span: variant.name_span,
|
|
payload_ty: variant.payload_ty.clone(),
|
|
payload_ty_span: variant.payload_ty_span,
|
|
});
|
|
}
|
|
|
|
if errors.is_empty() {
|
|
Ok((
|
|
CheckedEnum {
|
|
name: enum_decl.name.clone(),
|
|
variants: variants
|
|
.iter()
|
|
.map(|variant| CheckedEnumVariant {
|
|
name: variant.name.clone(),
|
|
payload_ty: variant.payload_ty.clone(),
|
|
})
|
|
.collect(),
|
|
span: enum_decl.span,
|
|
file: file.to_string(),
|
|
},
|
|
EnumSig {
|
|
span: enum_decl.name_span,
|
|
variants,
|
|
variants_by_name,
|
|
},
|
|
))
|
|
} else {
|
|
Err(errors)
|
|
}
|
|
}
|
|
|
|
fn enum_payload_type_supported(ty: &Type, declared_struct_names: &HashMap<String, Span>) -> bool {
|
|
matches!(
|
|
ty,
|
|
Type::I32 | Type::I64 | Type::U32 | Type::U64 | Type::F64 | Type::Bool | Type::String
|
|
) || matches!(ty, Type::Named(name) if declared_struct_names.contains_key(name))
|
|
}
|
|
|
|
fn supported_enum_payload_message() -> &'static str {
|
|
"enum payload variants support only unary direct `i32`, `i64`, `u32`, `u64`, `f64`, `bool`, `string`, or known non-recursive struct payloads"
|
|
}
|
|
|
|
fn supported_enum_payload_expected() -> &'static str {
|
|
"i32, i64, u32, u64, f64, bool, string, or known non-recursive struct type"
|
|
}
|
|
|
|
fn enum_payload_equality_supported(ty: &Type, enums: &HashMap<String, EnumSig>) -> bool {
|
|
match ty {
|
|
Type::Named(name) => enums
|
|
.get(name)
|
|
.and_then(|sig| {
|
|
sig.variants
|
|
.iter()
|
|
.find_map(|variant| variant.payload_ty.as_ref())
|
|
})
|
|
.map(enum_scalar_string_payload_type_supported)
|
|
.unwrap_or(true),
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
fn enum_scalar_string_payload_type_supported(ty: &Type) -> bool {
|
|
matches!(
|
|
ty,
|
|
Type::I32 | Type::I64 | Type::U32 | Type::U64 | Type::F64 | Type::Bool | Type::String
|
|
)
|
|
}
|
|
|
|
fn validate_enum_payload_struct_cycles(
|
|
file: &str,
|
|
enums: &HashMap<String, EnumSig>,
|
|
structs: &HashMap<String, StructSig>,
|
|
) -> Vec<Diagnostic> {
|
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
enum Node {
|
|
Enum(String),
|
|
Struct(String),
|
|
}
|
|
|
|
impl Node {
|
|
fn name(&self) -> &str {
|
|
match self {
|
|
Self::Enum(name) | Self::Struct(name) => name,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn visit(
|
|
file: &str,
|
|
node: Node,
|
|
enums: &HashMap<String, EnumSig>,
|
|
structs: &HashMap<String, StructSig>,
|
|
stack: &mut Vec<Node>,
|
|
visited: &mut HashSet<Node>,
|
|
errors: &mut Vec<Diagnostic>,
|
|
) {
|
|
if !visited.insert(node.clone()) {
|
|
return;
|
|
}
|
|
|
|
stack.push(node.clone());
|
|
|
|
match &node {
|
|
Node::Enum(enum_name) => {
|
|
let Some(sig) = enums.get(enum_name) else {
|
|
stack.pop();
|
|
return;
|
|
};
|
|
|
|
for variant in &sig.variants {
|
|
let Some(Type::Named(struct_name)) = &variant.payload_ty else {
|
|
continue;
|
|
};
|
|
let Some(struct_sig) = structs.get(struct_name) else {
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"UnsupportedEnumPayloadType",
|
|
supported_enum_payload_message(),
|
|
)
|
|
.with_span(variant.payload_ty_span.unwrap_or(variant.name_span))
|
|
.expected(supported_enum_payload_expected())
|
|
.found(struct_name.clone())
|
|
.hint("use a direct scalar/string payload, a checked non-recursive struct payload, or keep the variant payloadless"),
|
|
);
|
|
continue;
|
|
};
|
|
|
|
let target = Node::Struct(struct_name.clone());
|
|
if let Some(index) = stack.iter().position(|entry| entry == &target) {
|
|
let mut cycle = stack[index..]
|
|
.iter()
|
|
.map(|entry| entry.name().to_string())
|
|
.collect::<Vec<_>>();
|
|
cycle.push(struct_name.clone());
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"RecursiveEnumPayloadStructUnsupported",
|
|
format!(
|
|
"recursive enum payload/struct cycles are not supported (`{}`)",
|
|
cycle.join(" -> ")
|
|
),
|
|
)
|
|
.with_span(variant.payload_ty_span.unwrap_or(variant.name_span))
|
|
.related("recursive struct declaration", struct_sig.span)
|
|
.hint("break the cycle by removing the struct payload edge or a direct enum/struct field edge"),
|
|
);
|
|
continue;
|
|
}
|
|
|
|
visit(file, target, enums, structs, stack, visited, errors);
|
|
}
|
|
}
|
|
Node::Struct(struct_name) => {
|
|
let Some(sig) = structs.get(struct_name) else {
|
|
stack.pop();
|
|
return;
|
|
};
|
|
|
|
for field in &sig.fields {
|
|
let target = match &field.ty {
|
|
Type::Named(target) if structs.contains_key(target) => {
|
|
Node::Struct(target.clone())
|
|
}
|
|
Type::Named(target) if enums.contains_key(target) => {
|
|
Node::Enum(target.clone())
|
|
}
|
|
_ => continue,
|
|
};
|
|
|
|
let related_span = match &target {
|
|
Node::Struct(target_name) => structs
|
|
.get(target_name)
|
|
.map(|target_sig| ("recursive struct declaration", target_sig.span)),
|
|
Node::Enum(target_name) => enums
|
|
.get(target_name)
|
|
.map(|target_sig| ("recursive enum declaration", target_sig.span)),
|
|
};
|
|
|
|
if let Some(index) = stack.iter().position(|entry| entry == &target) {
|
|
let mut cycle = stack[index..]
|
|
.iter()
|
|
.map(|entry| entry.name().to_string())
|
|
.collect::<Vec<_>>();
|
|
cycle.push(target.name().to_string());
|
|
let diagnostic = Diagnostic::new(
|
|
file,
|
|
"RecursiveEnumPayloadStructUnsupported",
|
|
format!(
|
|
"recursive enum payload/struct cycles are not supported (`{}`)",
|
|
cycle.join(" -> ")
|
|
),
|
|
)
|
|
.with_span(field.ty_span)
|
|
.hint("break the cycle by removing the struct payload edge or a direct enum/struct field edge");
|
|
errors.push(if let Some((label, span)) = related_span {
|
|
diagnostic.related(label, span)
|
|
} else {
|
|
diagnostic
|
|
});
|
|
continue;
|
|
}
|
|
|
|
visit(file, target, enums, structs, stack, visited, errors);
|
|
}
|
|
}
|
|
}
|
|
|
|
stack.pop();
|
|
}
|
|
|
|
let mut errors = Vec::new();
|
|
let mut visited = HashSet::new();
|
|
let mut enum_names = enums.keys().cloned().collect::<Vec<_>>();
|
|
enum_names.sort();
|
|
for name in enum_names {
|
|
visit(
|
|
file,
|
|
Node::Enum(name),
|
|
enums,
|
|
structs,
|
|
&mut Vec::new(),
|
|
&mut visited,
|
|
&mut errors,
|
|
);
|
|
}
|
|
errors
|
|
}
|
|
|
|
fn external_enum_sig(enum_decl: &ExternalEnum) -> EnumSig {
|
|
let mut variants = Vec::new();
|
|
let mut variants_by_name = HashMap::new();
|
|
|
|
for variant in &enum_decl.variants {
|
|
variants_by_name.insert(variant.name.clone(), variants.len());
|
|
variants.push(EnumVariantSig {
|
|
name: variant.name.clone(),
|
|
name_span: variant.name_span,
|
|
payload_ty: variant.payload_ty.clone(),
|
|
payload_ty_span: variant.payload_ty_span,
|
|
});
|
|
}
|
|
|
|
EnumSig {
|
|
span: enum_decl.span,
|
|
variants,
|
|
variants_by_name,
|
|
}
|
|
}
|
|
|
|
pub fn print_checked_program(program: &CheckedProgram) -> String {
|
|
let mut output = String::new();
|
|
|
|
output.push_str("program ");
|
|
output.push_str(&program.module);
|
|
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 (name, ty) in &struct_decl.fields {
|
|
output.push_str(" field ");
|
|
output.push_str(name);
|
|
output.push_str(": ");
|
|
output.push_str(&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, (name, ty)) in import.params.iter().enumerate() {
|
|
if index > 0 {
|
|
output.push_str(", ");
|
|
}
|
|
output.push_str(name);
|
|
output.push_str(": ");
|
|
output.push_str(&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, (name, ty)) in function.params.iter().enumerate() {
|
|
if index > 0 {
|
|
output.push_str(", ");
|
|
}
|
|
output.push_str(name);
|
|
output.push_str(": ");
|
|
output.push_str(&ty.to_string());
|
|
}
|
|
output.push_str(") -> ");
|
|
output.push_str(&function.return_type.to_string());
|
|
output.push('\n');
|
|
|
|
for expr in &function.body {
|
|
write_checked_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_checked_expr(expr, 2, &mut output);
|
|
}
|
|
}
|
|
|
|
output
|
|
}
|
|
|
|
fn write_checked_expr(expr: &TExpr, indent: usize, output: &mut String) {
|
|
output.push_str(&" ".repeat(indent));
|
|
|
|
match &expr.kind {
|
|
TExprKind::Int(value) => {
|
|
output.push_str("int ");
|
|
output.push_str(&value.to_string());
|
|
}
|
|
TExprKind::Int64(value) => {
|
|
output.push_str("i64 ");
|
|
output.push_str(&value.to_string());
|
|
}
|
|
TExprKind::UInt32(value) => {
|
|
output.push_str("u32 ");
|
|
output.push_str(&value.to_string());
|
|
}
|
|
TExprKind::UInt64(value) => {
|
|
output.push_str("u64 ");
|
|
output.push_str(&value.to_string());
|
|
}
|
|
TExprKind::Float(value) => {
|
|
output.push_str("float ");
|
|
output.push_str(&value.to_string());
|
|
}
|
|
TExprKind::Bool(value) => {
|
|
output.push_str("bool ");
|
|
output.push_str(&value.to_string());
|
|
}
|
|
TExprKind::String(value) => {
|
|
output.push_str("string \"");
|
|
for ch in value.chars() {
|
|
output.extend(ch.escape_default());
|
|
}
|
|
output.push('"');
|
|
}
|
|
TExprKind::Var(name) => {
|
|
output.push_str("var ");
|
|
output.push_str(name);
|
|
}
|
|
TExprKind::StructInit { name, fields } => {
|
|
output.push_str("construct ");
|
|
output.push_str(name);
|
|
write_checked_type(&expr.ty, output);
|
|
for (field, value) in fields {
|
|
output.push_str(&" ".repeat(indent + 1));
|
|
output.push_str("field ");
|
|
output.push_str(field);
|
|
output.push('\n');
|
|
write_checked_expr(value, indent + 2, output);
|
|
}
|
|
return;
|
|
}
|
|
TExprKind::ArrayInit { elements } => {
|
|
output.push_str("array");
|
|
write_checked_type(&expr.ty, output);
|
|
for element in elements {
|
|
write_checked_expr(element, indent + 1, output);
|
|
}
|
|
return;
|
|
}
|
|
TExprKind::OptionSome { value } => {
|
|
output.push_str("some");
|
|
write_checked_type(&expr.ty, output);
|
|
write_checked_expr(value, indent + 1, output);
|
|
return;
|
|
}
|
|
TExprKind::OptionNone => {
|
|
output.push_str("none");
|
|
}
|
|
TExprKind::ResultOk { value } => {
|
|
output.push_str("ok");
|
|
write_checked_type(&expr.ty, output);
|
|
write_checked_expr(value, indent + 1, output);
|
|
return;
|
|
}
|
|
TExprKind::ResultErr { value } => {
|
|
output.push_str("err");
|
|
write_checked_type(&expr.ty, output);
|
|
write_checked_expr(value, indent + 1, output);
|
|
return;
|
|
}
|
|
TExprKind::OptionIsSome { value } => {
|
|
output.push_str("is_some");
|
|
write_checked_type(&expr.ty, output);
|
|
write_checked_expr(value, indent + 1, output);
|
|
return;
|
|
}
|
|
TExprKind::OptionIsNone { value } => {
|
|
output.push_str("is_none");
|
|
write_checked_type(&expr.ty, output);
|
|
write_checked_expr(value, indent + 1, output);
|
|
return;
|
|
}
|
|
TExprKind::OptionUnwrapSome { value } => {
|
|
output.push_str("unwrap_some");
|
|
write_checked_type(&expr.ty, output);
|
|
write_checked_expr(value, indent + 1, output);
|
|
return;
|
|
}
|
|
TExprKind::ResultIsOk { source_name, value } => {
|
|
output.push_str(source_name);
|
|
write_checked_type(&expr.ty, output);
|
|
write_checked_expr(value, indent + 1, output);
|
|
return;
|
|
}
|
|
TExprKind::ResultIsErr { source_name, value } => {
|
|
output.push_str(source_name);
|
|
write_checked_type(&expr.ty, output);
|
|
write_checked_expr(value, indent + 1, output);
|
|
return;
|
|
}
|
|
TExprKind::ResultUnwrapOk { source_name, value } => {
|
|
output.push_str(source_name);
|
|
write_checked_type(&expr.ty, output);
|
|
write_checked_expr(value, indent + 1, output);
|
|
return;
|
|
}
|
|
TExprKind::ResultUnwrapErr { source_name, value } => {
|
|
output.push_str(source_name);
|
|
write_checked_type(&expr.ty, output);
|
|
write_checked_expr(value, indent + 1, output);
|
|
return;
|
|
}
|
|
TExprKind::EnumVariant {
|
|
enum_name,
|
|
variant,
|
|
discriminant,
|
|
payload,
|
|
} => {
|
|
output.push_str("enum-variant ");
|
|
output.push_str(enum_name);
|
|
output.push('.');
|
|
output.push_str(variant);
|
|
output.push_str(" #");
|
|
output.push_str(&discriminant.to_string());
|
|
if let Some(payload) = payload {
|
|
output.push_str(" payload");
|
|
write_checked_type(&expr.ty, output);
|
|
write_checked_expr(payload, indent + 1, output);
|
|
return;
|
|
}
|
|
}
|
|
TExprKind::FieldAccess { value, field } => {
|
|
output.push_str("field-access ");
|
|
output.push_str(field);
|
|
write_checked_type(&expr.ty, output);
|
|
write_checked_expr(value, indent + 1, output);
|
|
return;
|
|
}
|
|
TExprKind::Index { array, index } => {
|
|
output.push_str("index");
|
|
write_checked_type(&expr.ty, output);
|
|
write_checked_expr(array, indent + 1, output);
|
|
write_checked_expr(index, indent + 1, output);
|
|
return;
|
|
}
|
|
TExprKind::Local {
|
|
mutable,
|
|
name,
|
|
initializer,
|
|
} => {
|
|
output.push_str(if *mutable { "local var " } else { "local let " });
|
|
output.push_str(name);
|
|
write_checked_type(&expr.ty, output);
|
|
write_checked_expr(initializer, indent + 1, output);
|
|
return;
|
|
}
|
|
TExprKind::Set { name, expr: value } => {
|
|
output.push_str("set ");
|
|
output.push_str(name);
|
|
write_checked_type(&expr.ty, output);
|
|
write_checked_expr(value, indent + 1, output);
|
|
return;
|
|
}
|
|
TExprKind::Binary { op, left, right } => {
|
|
output.push_str("binary ");
|
|
output.push_str(lower::binary_op_name(*op));
|
|
write_checked_type(&expr.ty, output);
|
|
write_checked_expr(left, indent + 1, output);
|
|
write_checked_expr(right, indent + 1, output);
|
|
return;
|
|
}
|
|
TExprKind::If {
|
|
condition,
|
|
then_expr,
|
|
else_expr,
|
|
} => {
|
|
output.push_str("if");
|
|
write_checked_type(&expr.ty, output);
|
|
write_checked_expr(condition, indent + 1, output);
|
|
write_checked_expr(then_expr, indent + 1, output);
|
|
write_checked_expr(else_expr, indent + 1, output);
|
|
return;
|
|
}
|
|
TExprKind::Match { subject, arms } => {
|
|
output.push_str("match");
|
|
write_checked_type(&expr.ty, output);
|
|
output.push_str(&" ".repeat(indent + 1));
|
|
output.push_str("subject\n");
|
|
write_checked_expr(subject, indent + 2, output);
|
|
for arm in arms {
|
|
output.push_str(&" ".repeat(indent + 1));
|
|
output.push_str("arm ");
|
|
output.push_str(&lower::match_pattern_name(&arm.pattern));
|
|
if let Some(binding) = &arm.binding {
|
|
output.push(' ');
|
|
output.push_str(binding);
|
|
}
|
|
output.push('\n');
|
|
for expr in &arm.body {
|
|
write_checked_expr(expr, indent + 2, output);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
TExprKind::While { condition, body } => {
|
|
output.push_str("while");
|
|
write_checked_type(&expr.ty, output);
|
|
write_checked_expr(condition, indent + 1, output);
|
|
for expr in body {
|
|
write_checked_expr(expr, indent + 1, output);
|
|
}
|
|
return;
|
|
}
|
|
TExprKind::Unsafe { body } => {
|
|
output.push_str("unsafe");
|
|
write_checked_type(&expr.ty, output);
|
|
for expr in body {
|
|
write_checked_expr(expr, indent + 1, output);
|
|
}
|
|
return;
|
|
}
|
|
TExprKind::Call { name, args } => {
|
|
output.push_str("call ");
|
|
output.push_str(name);
|
|
write_checked_type(&expr.ty, output);
|
|
for arg in args {
|
|
write_checked_expr(arg, indent + 1, output);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
write_checked_type(&expr.ty, output);
|
|
}
|
|
|
|
fn write_checked_type(ty: &Type, output: &mut String) {
|
|
output.push_str(" : ");
|
|
output.push_str(&ty.to_string());
|
|
output.push('\n');
|
|
}
|
|
|
|
fn check_function(
|
|
file: &str,
|
|
function: Function,
|
|
functions: &HashMap<String, FnSig>,
|
|
structs: &HashMap<String, StructSig>,
|
|
enums: &HashMap<String, EnumSig>,
|
|
) -> Result<CheckedFunction, Vec<Diagnostic>> {
|
|
let mut locals = HashMap::new();
|
|
let mut errors = Vec::new();
|
|
|
|
for param in &function.params {
|
|
if is_reserved_callable_name(¶m.name) {
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"ParameterShadowsCallable",
|
|
format!(
|
|
"parameter `{}` conflicts with a compiler-known callable name",
|
|
param.name
|
|
),
|
|
)
|
|
.with_span(param.name_span)
|
|
.hint("choose a parameter name distinct from compiler-known callable names"),
|
|
);
|
|
}
|
|
|
|
locals.insert(
|
|
param.name.clone(),
|
|
Binding {
|
|
ty: param.ty.clone(),
|
|
mutable: false,
|
|
kind: BindingKind::Param,
|
|
span: param.name_span,
|
|
},
|
|
);
|
|
}
|
|
|
|
validate_unit_signature_types(file, &function, &mut errors);
|
|
validate_named_signature_types(file, &function, structs, enums, &mut errors);
|
|
validate_array_signature_types(file, &function, structs, enums, &mut errors);
|
|
validate_vector_signature_types(file, &function, &mut errors);
|
|
validate_option_result_signature_types(file, &function, &mut errors);
|
|
validate_enum_container_signature_types(file, &function, enums, &mut errors);
|
|
let source_body_is_empty = function.body.is_empty();
|
|
let body = check_body(
|
|
file,
|
|
&function.body,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
false,
|
|
&mut errors,
|
|
);
|
|
|
|
if source_body_is_empty {
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"EmptyFunction",
|
|
format!("function `{}` has no body", function.name),
|
|
)
|
|
.with_span(function.span),
|
|
);
|
|
} else if errors.is_empty() {
|
|
if let Some(last) = body.last() {
|
|
if last.ty != function.return_type {
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"ReturnTypeMismatch",
|
|
format!("function `{}` returns wrong type", function.name),
|
|
)
|
|
.with_span(function.span)
|
|
.expected(function.return_type.to_string())
|
|
.found(last.ty.to_string()),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if errors.is_empty() {
|
|
Ok(CheckedFunction {
|
|
name: function.name,
|
|
params: function
|
|
.params
|
|
.into_iter()
|
|
.map(|p| (p.name, p.ty))
|
|
.collect(),
|
|
return_type: function.return_type,
|
|
body,
|
|
span: function.span,
|
|
file: file.to_string(),
|
|
})
|
|
} else {
|
|
Err(errors)
|
|
}
|
|
}
|
|
|
|
fn check_test(
|
|
file: &str,
|
|
test: Test,
|
|
functions: &HashMap<String, FnSig>,
|
|
structs: &HashMap<String, StructSig>,
|
|
enums: &HashMap<String, EnumSig>,
|
|
) -> Result<CheckedTest, Vec<Diagnostic>> {
|
|
let locals = HashMap::new();
|
|
let mut errors = Vec::new();
|
|
let body = check_body(
|
|
file,
|
|
&test.body,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
false,
|
|
&mut errors,
|
|
);
|
|
|
|
if errors.is_empty() {
|
|
if let Some(expr) = body.last() {
|
|
if expr.ty != Type::Bool {
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"TestExpressionNotBool",
|
|
format!("test `{}` must evaluate to bool", test.name),
|
|
)
|
|
.with_span(expr.span)
|
|
.expected(Type::Bool.to_string())
|
|
.found(expr.ty.to_string()),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if errors.is_empty() {
|
|
Ok(CheckedTest {
|
|
name: test.name,
|
|
name_span: test.name_span,
|
|
body,
|
|
span: test.span,
|
|
file: file.to_string(),
|
|
})
|
|
} else {
|
|
Err(errors)
|
|
}
|
|
}
|
|
|
|
fn validate_array_signature_types(
|
|
file: &str,
|
|
function: &Function,
|
|
structs: &HashMap<String, StructSig>,
|
|
enums: &HashMap<String, EnumSig>,
|
|
errors: &mut Vec<Diagnostic>,
|
|
) {
|
|
for param in &function.params {
|
|
if matches!(¶m.ty, Type::Array(_, _)) {
|
|
if let Err(err) = check_array_type(file, ¶m.ty, param.ty_span, structs, enums) {
|
|
errors.push(err);
|
|
}
|
|
}
|
|
}
|
|
|
|
if matches!(&function.return_type, Type::Array(_, _)) {
|
|
if let Err(err) = check_array_type(
|
|
file,
|
|
&function.return_type,
|
|
function.return_type_span,
|
|
structs,
|
|
enums,
|
|
) {
|
|
errors.push(err);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn validate_vector_signature_types(file: &str, function: &Function, errors: &mut Vec<Diagnostic>) {
|
|
for param in &function.params {
|
|
if matches!(¶m.ty, Type::Vec(_)) {
|
|
if let Err(err) = check_vector_type(file, ¶m.ty, param.ty_span) {
|
|
errors.push(err);
|
|
}
|
|
}
|
|
}
|
|
|
|
if matches!(&function.return_type, Type::Vec(_)) {
|
|
if let Err(err) = check_vector_type(file, &function.return_type, function.return_type_span)
|
|
{
|
|
errors.push(err);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn validate_unit_signature_types(file: &str, function: &Function, errors: &mut Vec<Diagnostic>) {
|
|
for param in &function.params {
|
|
if param.ty == Type::Unit {
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"UnsupportedUnitSignatureType",
|
|
"function parameter type `unit` is unsupported",
|
|
)
|
|
.with_span(param.ty_span)
|
|
.expected("non-unit function parameter type")
|
|
.found(Type::Unit.to_string())
|
|
.hint("`unit` is reserved for compiler/runtime unit-producing forms"),
|
|
);
|
|
}
|
|
}
|
|
|
|
if function.return_type == Type::Unit {
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"UnsupportedUnitSignatureType",
|
|
"function return type `unit` is unsupported",
|
|
)
|
|
.with_span(function.return_type_span)
|
|
.expected("non-unit function return type")
|
|
.found(Type::Unit.to_string())
|
|
.hint("`unit` is reserved for compiler/runtime unit-producing forms"),
|
|
);
|
|
}
|
|
}
|
|
|
|
fn validate_named_signature_types(
|
|
file: &str,
|
|
function: &Function,
|
|
structs: &HashMap<String, StructSig>,
|
|
enums: &HashMap<String, EnumSig>,
|
|
errors: &mut Vec<Diagnostic>,
|
|
) {
|
|
for param in &function.params {
|
|
if let Type::Named(name) = ¶m.ty {
|
|
if !structs.contains_key(name) && !enums.contains_key(name) {
|
|
errors.push(unknown_named_type(
|
|
file,
|
|
param.ty_span,
|
|
name,
|
|
"function parameter",
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Type::Named(name) = &function.return_type {
|
|
if !structs.contains_key(name) && !enums.contains_key(name) {
|
|
errors.push(unknown_named_type(
|
|
file,
|
|
function.return_type_span,
|
|
name,
|
|
"function return",
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
fn validate_option_result_signature_types(
|
|
file: &str,
|
|
function: &Function,
|
|
errors: &mut Vec<Diagnostic>,
|
|
) {
|
|
for param in &function.params {
|
|
if let Err(err) = check_option_result_type(file, ¶m.ty, param.ty_span) {
|
|
errors.push(err);
|
|
}
|
|
}
|
|
|
|
if let Err(err) =
|
|
check_option_result_type(file, &function.return_type, function.return_type_span)
|
|
{
|
|
errors.push(err);
|
|
}
|
|
}
|
|
|
|
fn validate_enum_container_signature_types(
|
|
file: &str,
|
|
function: &Function,
|
|
enums: &HashMap<String, EnumSig>,
|
|
errors: &mut Vec<Diagnostic>,
|
|
) {
|
|
for param in &function.params {
|
|
if !matches!(param.ty, Type::Named(_) | Type::Array(_, _))
|
|
&& contains_enum_type(¶m.ty, enums)
|
|
{
|
|
errors.push(unsupported_enum_container(file, param.ty_span, ¶m.ty));
|
|
}
|
|
}
|
|
|
|
if !matches!(function.return_type, Type::Named(_) | Type::Array(_, _))
|
|
&& contains_enum_type(&function.return_type, enums)
|
|
{
|
|
errors.push(unsupported_enum_container(
|
|
file,
|
|
function.return_type_span,
|
|
&function.return_type,
|
|
));
|
|
}
|
|
}
|
|
|
|
fn is_known_struct_type(ty: &Type, structs: &HashMap<String, StructSig>) -> bool {
|
|
matches!(ty, Type::Named(name) if structs.contains_key(name))
|
|
}
|
|
|
|
fn is_known_enum_type(ty: &Type, enums: &HashMap<String, EnumSig>) -> bool {
|
|
matches!(ty, Type::Named(name) if enums.contains_key(name))
|
|
}
|
|
|
|
fn is_enum_type(ty: &Type, enums: &HashMap<String, EnumSig>) -> bool {
|
|
is_known_enum_type(ty, enums)
|
|
}
|
|
|
|
fn is_array_type(ty: &Type) -> bool {
|
|
matches!(ty, Type::Array(_, _))
|
|
}
|
|
|
|
fn is_vector_type(ty: &Type) -> bool {
|
|
matches!(ty, Type::Vec(_))
|
|
}
|
|
|
|
fn is_vec_i32_type(ty: &Type) -> bool {
|
|
matches!(ty, Type::Vec(inner) if **inner == Type::I32)
|
|
}
|
|
|
|
fn is_vec_i64_type(ty: &Type) -> bool {
|
|
matches!(ty, Type::Vec(inner) if **inner == Type::I64)
|
|
}
|
|
|
|
fn is_vec_f64_type(ty: &Type) -> bool {
|
|
matches!(ty, Type::Vec(inner) if **inner == Type::F64)
|
|
}
|
|
|
|
fn is_vec_bool_type(ty: &Type) -> bool {
|
|
matches!(ty, Type::Vec(inner) if **inner == Type::Bool)
|
|
}
|
|
|
|
fn is_vec_string_type(ty: &Type) -> bool {
|
|
matches!(ty, Type::Vec(inner) if **inner == Type::String)
|
|
}
|
|
|
|
fn is_option_result_type(ty: &Type) -> bool {
|
|
matches!(ty, Type::Option(_) | Type::Result(_, _))
|
|
}
|
|
|
|
fn check_body(
|
|
file: &str,
|
|
body: &[Expr],
|
|
mut locals: HashMap<String, Binding>,
|
|
functions: &HashMap<String, FnSig>,
|
|
structs: &HashMap<String, StructSig>,
|
|
enums: &HashMap<String, EnumSig>,
|
|
unsafe_context: bool,
|
|
errors: &mut Vec<Diagnostic>,
|
|
) -> Vec<TExpr> {
|
|
let mut checked = Vec::new();
|
|
|
|
for expr in body {
|
|
match &expr.kind {
|
|
ExprKind::Local {
|
|
mutable,
|
|
name,
|
|
name_span,
|
|
ty,
|
|
expr: initializer,
|
|
} => {
|
|
let checked_initializer = match check_expr(
|
|
file,
|
|
initializer,
|
|
&locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
) {
|
|
Ok(initializer) => Some(initializer),
|
|
Err(err) => {
|
|
errors.push(err);
|
|
None
|
|
}
|
|
};
|
|
|
|
let local_type_supported =
|
|
match check_local_type(file, ty, *mutable, *name_span, structs, enums) {
|
|
Ok(()) => true,
|
|
Err(err) => {
|
|
errors.push(err);
|
|
false
|
|
}
|
|
};
|
|
|
|
if let Some(existing) = locals.get(name) {
|
|
match existing.kind {
|
|
BindingKind::Param => {
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"LocalRedeclaresParameter",
|
|
format!("local `{}` redeclares a parameter", name),
|
|
)
|
|
.with_span(*name_span)
|
|
.hint("choose a local name distinct from parameters"),
|
|
);
|
|
}
|
|
BindingKind::MatchPayload => {
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"MatchBindingCollision",
|
|
format!(
|
|
"local `{}` collides with a match payload binding",
|
|
name
|
|
),
|
|
)
|
|
.with_span(*name_span)
|
|
.related("match payload binding", existing.span),
|
|
);
|
|
}
|
|
BindingKind::Local => {
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"DuplicateLocal",
|
|
format!("duplicate local declaration `{}`", name),
|
|
)
|
|
.with_span(*name_span)
|
|
.related("original local declaration", existing.span),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if functions.contains_key(name) || is_reserved_callable_name(name) {
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"LocalShadowsCallable",
|
|
format!(
|
|
"local `{}` conflicts with a function or compiler intrinsic",
|
|
name
|
|
),
|
|
)
|
|
.with_span(*name_span)
|
|
.hint(
|
|
"choose a local name distinct from functions and compiler intrinsics",
|
|
),
|
|
);
|
|
}
|
|
|
|
if structs.contains_key(name) {
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"LocalShadowsStruct",
|
|
format!("local `{}` conflicts with a struct", name),
|
|
)
|
|
.with_span(*name_span)
|
|
.hint("choose a local name distinct from structs"),
|
|
);
|
|
}
|
|
|
|
if enums.contains_key(name) {
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"LocalShadowsEnum",
|
|
format!("local `{}` conflicts with an enum", name),
|
|
)
|
|
.with_span(*name_span)
|
|
.hint("choose a local name distinct from enums"),
|
|
);
|
|
}
|
|
|
|
let Some(initializer) = checked_initializer else {
|
|
continue;
|
|
};
|
|
|
|
if local_type_supported && initializer.ty != *ty {
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"TypeMismatch",
|
|
format!("local `{}` initializer has the wrong type", name),
|
|
)
|
|
.with_span(initializer.span)
|
|
.expected(ty.to_string())
|
|
.found(initializer.ty.to_string()),
|
|
);
|
|
continue;
|
|
}
|
|
|
|
if local_type_supported && !locals.contains_key(name) {
|
|
locals.insert(
|
|
name.clone(),
|
|
Binding {
|
|
ty: ty.clone(),
|
|
mutable: *mutable,
|
|
kind: BindingKind::Local,
|
|
span: *name_span,
|
|
},
|
|
);
|
|
|
|
checked.push(TExpr {
|
|
ty: Type::Unit,
|
|
span: expr.span,
|
|
kind: TExprKind::Local {
|
|
mutable: *mutable,
|
|
name: name.clone(),
|
|
initializer: Box::new(initializer),
|
|
},
|
|
});
|
|
}
|
|
}
|
|
_ => match check_expr(
|
|
file,
|
|
expr,
|
|
&locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
) {
|
|
Ok(texpr) => checked.push(texpr),
|
|
Err(err) => errors.push(err),
|
|
},
|
|
}
|
|
}
|
|
|
|
checked
|
|
}
|
|
|
|
fn check_local_type(
|
|
file: &str,
|
|
ty: &Type,
|
|
mutable: bool,
|
|
span: Span,
|
|
structs: &HashMap<String, StructSig>,
|
|
enums: &HashMap<String, EnumSig>,
|
|
) -> Result<(), Diagnostic> {
|
|
match ty {
|
|
Type::I32 => Ok(()),
|
|
Type::Bool => Ok(()),
|
|
Type::I64 => Ok(()),
|
|
Type::U32 => Ok(()),
|
|
Type::U64 => Ok(()),
|
|
Type::F64 => Ok(()),
|
|
Type::String => Ok(()),
|
|
Type::Array(_, _) => {
|
|
check_array_type(file, ty, span, structs, enums)?;
|
|
if mutable {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"MutableArrayLocalUnsupported",
|
|
"first-pass arrays can be stored only in immutable locals",
|
|
)
|
|
.with_span(span)
|
|
.hint("declare array locals with `let`"));
|
|
}
|
|
Ok(())
|
|
}
|
|
Type::Vec(_) => {
|
|
if contains_enum_type(ty, enums) {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"UnsupportedEnumContainer",
|
|
"enum values in vector containers are not supported",
|
|
)
|
|
.with_span(span)
|
|
.hint("store enum values directly in immutable locals"));
|
|
}
|
|
check_vector_type(file, ty, span)?;
|
|
Ok(())
|
|
}
|
|
Type::Option(_) | Type::Result(_, _) => {
|
|
if contains_enum_type(ty, enums) {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"UnsupportedEnumContainer",
|
|
"enum values in option/result containers are not supported",
|
|
)
|
|
.with_span(span)
|
|
.hint("store enum values directly in immutable locals"));
|
|
}
|
|
check_option_result_type(file, ty, span)?;
|
|
Ok(())
|
|
}
|
|
ty if is_known_struct_type(ty, structs) => Ok(()),
|
|
ty if is_known_enum_type(ty, enums) => Ok(()),
|
|
Type::Named(name) => Err(unknown_named_type(file, span, name, "local declaration")),
|
|
_ => Err(Diagnostic::new(
|
|
file,
|
|
"UnsupportedLocalType",
|
|
"local variables support `i32`, `bool`, `i64`, `u32`, `u64`, `f64`, `string`, immutable direct-scalar-or-string arrays, concrete vectors, option/result, known struct values, and enum values",
|
|
)
|
|
.with_span(span)
|
|
.expected("i32, bool, i64, u32, u64, f64, string, (array i32 N), (array i64 N), (array u32 N), (array u64 N), (array f64 N), (array bool N), (array string N), (vec i32), (vec i64), (vec f64), (vec bool), (vec string), option/result, known struct, or enum")
|
|
.found(ty.to_string())),
|
|
}
|
|
}
|
|
|
|
fn unknown_named_type(file: &str, span: Span, name: &str, context: &str) -> Diagnostic {
|
|
if is_generic_type_parameter_name(name) {
|
|
return unsupported_generic_type_parameter(file, span, name);
|
|
}
|
|
|
|
Diagnostic::new(
|
|
file,
|
|
"UnknownStructType",
|
|
format!("{} type `{}` is not a declared struct", context, name),
|
|
)
|
|
.with_span(span)
|
|
.expected("known struct")
|
|
.found(name.to_string())
|
|
.hint("declare the struct before using it as a type")
|
|
}
|
|
|
|
fn check_expr(
|
|
file: &str,
|
|
expr: &Expr,
|
|
locals: &HashMap<String, Binding>,
|
|
functions: &HashMap<String, FnSig>,
|
|
structs: &HashMap<String, StructSig>,
|
|
enums: &HashMap<String, EnumSig>,
|
|
unsafe_context: bool,
|
|
) -> Result<TExpr, Diagnostic> {
|
|
match &expr.kind {
|
|
ExprKind::Int(value) => Ok(TExpr {
|
|
ty: Type::I32,
|
|
span: expr.span,
|
|
kind: TExprKind::Int(*value),
|
|
}),
|
|
ExprKind::Int64(value) => Ok(TExpr {
|
|
ty: Type::I64,
|
|
span: expr.span,
|
|
kind: TExprKind::Int64(*value),
|
|
}),
|
|
ExprKind::UInt32(value) => Ok(TExpr {
|
|
ty: Type::U32,
|
|
span: expr.span,
|
|
kind: TExprKind::UInt32(*value),
|
|
}),
|
|
ExprKind::UInt64(value) => Ok(TExpr {
|
|
ty: Type::U64,
|
|
span: expr.span,
|
|
kind: TExprKind::UInt64(*value),
|
|
}),
|
|
ExprKind::Float(value) => Ok(TExpr {
|
|
ty: Type::F64,
|
|
span: expr.span,
|
|
kind: TExprKind::Float(*value),
|
|
}),
|
|
ExprKind::Bool(value) => Ok(TExpr {
|
|
ty: Type::Bool,
|
|
span: expr.span,
|
|
kind: TExprKind::Bool(*value),
|
|
}),
|
|
ExprKind::String(value) => Ok(TExpr {
|
|
ty: Type::String,
|
|
span: expr.span,
|
|
kind: TExprKind::String(value.clone()),
|
|
}),
|
|
ExprKind::Var(name) => {
|
|
let binding = locals.get(name).ok_or_else(|| {
|
|
Diagnostic::new(
|
|
file,
|
|
"UnknownVariable",
|
|
format!("unknown variable `{}`", name),
|
|
)
|
|
.with_span(expr.span)
|
|
})?;
|
|
|
|
Ok(TExpr {
|
|
ty: binding.ty.clone(),
|
|
span: expr.span,
|
|
kind: TExprKind::Var(name.clone()),
|
|
})
|
|
}
|
|
ExprKind::StructInit {
|
|
name,
|
|
name_span,
|
|
fields,
|
|
} => check_struct_init(
|
|
file,
|
|
expr,
|
|
name,
|
|
*name_span,
|
|
fields,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
),
|
|
ExprKind::ArrayInit {
|
|
elem_ty,
|
|
elem_ty_span,
|
|
elements,
|
|
} => check_array_init(
|
|
file,
|
|
expr,
|
|
elem_ty,
|
|
*elem_ty_span,
|
|
elements,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
),
|
|
ExprKind::OptionSome {
|
|
payload_ty,
|
|
payload_ty_span,
|
|
value,
|
|
} => check_option_some(
|
|
file,
|
|
expr,
|
|
payload_ty,
|
|
*payload_ty_span,
|
|
value,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
),
|
|
ExprKind::OptionNone {
|
|
payload_ty,
|
|
payload_ty_span,
|
|
} => check_option_none(file, expr, payload_ty, *payload_ty_span),
|
|
ExprKind::ResultOk {
|
|
ok_ty,
|
|
ok_ty_span,
|
|
err_ty,
|
|
err_ty_span,
|
|
value,
|
|
} => check_result_ok(
|
|
file,
|
|
expr,
|
|
ok_ty,
|
|
*ok_ty_span,
|
|
err_ty,
|
|
*err_ty_span,
|
|
value,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
),
|
|
ExprKind::ResultErr {
|
|
ok_ty,
|
|
ok_ty_span,
|
|
err_ty,
|
|
err_ty_span,
|
|
value,
|
|
} => check_result_err(
|
|
file,
|
|
expr,
|
|
ok_ty,
|
|
*ok_ty_span,
|
|
err_ty,
|
|
*err_ty_span,
|
|
value,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
),
|
|
ExprKind::OptionIsSome { value } => check_option_observer(
|
|
file,
|
|
expr,
|
|
value,
|
|
"is_some",
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
),
|
|
ExprKind::OptionIsNone { value } => check_option_observer(
|
|
file,
|
|
expr,
|
|
value,
|
|
"is_none",
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
),
|
|
ExprKind::OptionUnwrapSome { value } => check_option_unwrap(
|
|
file,
|
|
expr,
|
|
value,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
),
|
|
ExprKind::ResultIsOk { source_name, value } => check_result_observer(
|
|
file,
|
|
expr,
|
|
value,
|
|
source_name,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
),
|
|
ExprKind::ResultIsErr { source_name, value } => check_result_observer(
|
|
file,
|
|
expr,
|
|
value,
|
|
source_name,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
),
|
|
ExprKind::ResultUnwrapOk { source_name, value } => check_result_unwrap(
|
|
file,
|
|
expr,
|
|
value,
|
|
source_name,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
),
|
|
ExprKind::ResultUnwrapErr { source_name, value } => check_result_unwrap(
|
|
file,
|
|
expr,
|
|
value,
|
|
source_name,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
),
|
|
ExprKind::FieldAccess {
|
|
value,
|
|
field,
|
|
field_span,
|
|
} => check_field_access(
|
|
file,
|
|
expr,
|
|
value,
|
|
field,
|
|
*field_span,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
),
|
|
ExprKind::Index { array, index } => check_index(
|
|
file,
|
|
expr,
|
|
array,
|
|
index,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
),
|
|
ExprKind::EnumVariant {
|
|
enum_name,
|
|
variant,
|
|
name_span,
|
|
args,
|
|
} => check_enum_variant(
|
|
file,
|
|
expr,
|
|
enum_name,
|
|
variant,
|
|
*name_span,
|
|
args,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
),
|
|
ExprKind::Local { .. } => Err(Diagnostic::new(
|
|
file,
|
|
"LocalDeclarationNotAllowed",
|
|
"local declarations are allowed only as body forms",
|
|
)
|
|
.with_span(expr.span)
|
|
.hint("move the declaration before the final body expression")),
|
|
ExprKind::Set {
|
|
name,
|
|
name_span,
|
|
expr: value,
|
|
} => {
|
|
let Some(binding) = locals.get(name) else {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"UnknownVariable",
|
|
format!("unknown variable `{}`", name),
|
|
)
|
|
.with_span(*name_span));
|
|
};
|
|
|
|
if binding.kind == BindingKind::Param {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"CannotAssignParameter",
|
|
format!("cannot assign to parameter `{}`", name),
|
|
)
|
|
.with_span(*name_span));
|
|
}
|
|
|
|
if !binding.mutable {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"CannotAssignImmutableLocal",
|
|
format!("cannot assign to immutable local `{}`", name),
|
|
)
|
|
.with_span(*name_span)
|
|
.hint("declare it with `var` to make it mutable"));
|
|
}
|
|
|
|
let value = check_expr(
|
|
file,
|
|
value,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
)?;
|
|
if value.ty != binding.ty {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"TypeMismatch",
|
|
format!("cannot assign value of wrong type to `{}`", name),
|
|
)
|
|
.with_span(value.span)
|
|
.expected(binding.ty.to_string())
|
|
.found(value.ty.to_string()));
|
|
}
|
|
|
|
Ok(TExpr {
|
|
ty: Type::Unit,
|
|
span: expr.span,
|
|
kind: TExprKind::Set {
|
|
name: name.clone(),
|
|
expr: Box::new(value),
|
|
},
|
|
})
|
|
}
|
|
ExprKind::Binary { op, left, right } => {
|
|
let left = check_expr(
|
|
file,
|
|
left,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
)?;
|
|
let right = check_expr(
|
|
file,
|
|
right,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
)?;
|
|
|
|
match op {
|
|
BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul | BinaryOp::Div => {
|
|
if matches!(op, BinaryOp::Add)
|
|
&& (left.ty == Type::String || right.ty == Type::String)
|
|
{
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"UnsupportedStringConcatenation",
|
|
"string concatenation is not supported in the v1.2 runtime value slice",
|
|
)
|
|
.with_span(expr.span)
|
|
.hint("pass immutable string values through lets, parameters, returns, and calls without concatenating them"));
|
|
}
|
|
if left.ty == Type::F64 && right.ty == Type::F64 {
|
|
return Ok(TExpr {
|
|
ty: Type::F64,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
});
|
|
}
|
|
if left.ty == Type::I64 && right.ty == Type::I64 {
|
|
return Ok(TExpr {
|
|
ty: Type::I64,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
});
|
|
}
|
|
if left.ty == Type::U32 && right.ty == Type::U32 {
|
|
return Ok(TExpr {
|
|
ty: Type::U32,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
});
|
|
}
|
|
if left.ty == Type::U64 && right.ty == Type::U64 {
|
|
return Ok(TExpr {
|
|
ty: Type::U64,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
});
|
|
}
|
|
if left.ty == Type::U64 && right.ty == Type::U64 {
|
|
return Ok(TExpr {
|
|
ty: Type::U64,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
});
|
|
}
|
|
if left.ty == Type::U32 && right.ty == Type::U32 {
|
|
return Ok(TExpr {
|
|
ty: Type::U32,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
});
|
|
}
|
|
if is_numeric_primitive(&left.ty)
|
|
&& is_numeric_primitive(&right.ty)
|
|
&& left.ty != right.ty
|
|
{
|
|
return Err(numeric_operand_mismatch(file, expr, &left.ty, &right.ty));
|
|
}
|
|
expect_type(file, expr, &left.ty, &Type::I32)?;
|
|
expect_type(file, expr, &right.ty, &Type::I32)?;
|
|
Ok(TExpr {
|
|
ty: Type::I32,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
})
|
|
}
|
|
BinaryOp::Rem => {
|
|
if left.ty == Type::F64 || right.ty == Type::F64 {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"UnsupportedF64Remainder",
|
|
"floating-point remainder is not supported in the current numeric slice",
|
|
)
|
|
.with_span(expr.span)
|
|
.expected("i32 % i32, i64 % i64, u32 % u32, or u64 % u64")
|
|
.found(format!("{} and {}", left.ty, right.ty))
|
|
.hint("use integer remainder in exp-56; f64 remainder remains deferred"));
|
|
}
|
|
if left.ty == Type::I64 && right.ty == Type::I64 {
|
|
return Ok(TExpr {
|
|
ty: Type::I64,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
});
|
|
}
|
|
if left.ty == Type::U32 && right.ty == Type::U32 {
|
|
return Ok(TExpr {
|
|
ty: Type::U32,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
});
|
|
}
|
|
if left.ty == Type::U64 && right.ty == Type::U64 {
|
|
return Ok(TExpr {
|
|
ty: Type::U64,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
});
|
|
}
|
|
if left.ty == Type::U64 && right.ty == Type::U64 {
|
|
return Ok(TExpr {
|
|
ty: Type::U64,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
});
|
|
}
|
|
if left.ty == Type::U32 && right.ty == Type::U32 {
|
|
return Ok(TExpr {
|
|
ty: Type::U32,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
});
|
|
}
|
|
if is_numeric_primitive(&left.ty)
|
|
&& is_numeric_primitive(&right.ty)
|
|
&& left.ty != right.ty
|
|
{
|
|
return Err(numeric_operand_mismatch(file, expr, &left.ty, &right.ty));
|
|
}
|
|
expect_type(file, expr, &left.ty, &Type::I32)?;
|
|
expect_type(file, expr, &right.ty, &Type::I32)?;
|
|
Ok(TExpr {
|
|
ty: Type::I32,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
})
|
|
}
|
|
BinaryOp::BitAnd | BinaryOp::BitOr | BinaryOp::BitXor => {
|
|
if left.ty == Type::F64 || right.ty == Type::F64 {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"UnsupportedF64Bitwise",
|
|
"floating-point bitwise operations are not supported in the current numeric slice",
|
|
)
|
|
.with_span(expr.span)
|
|
.expected("i32 bitwise i32, i64 bitwise i64, u32 bitwise u32, or u64 bitwise u64")
|
|
.found(format!("{} and {}", left.ty, right.ty))
|
|
.hint("use integer bitwise operations in exp-57; f64 bitwise operations remain deferred"));
|
|
}
|
|
if left.ty == Type::I64 && right.ty == Type::I64 {
|
|
return Ok(TExpr {
|
|
ty: Type::I64,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
});
|
|
}
|
|
if left.ty == Type::U32 && right.ty == Type::U32 {
|
|
return Ok(TExpr {
|
|
ty: Type::U32,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
});
|
|
}
|
|
if left.ty == Type::U64 && right.ty == Type::U64 {
|
|
return Ok(TExpr {
|
|
ty: Type::U64,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
});
|
|
}
|
|
if left.ty == Type::U64 && right.ty == Type::U64 {
|
|
return Ok(TExpr {
|
|
ty: Type::U64,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
});
|
|
}
|
|
if left.ty == Type::U32 && right.ty == Type::U32 {
|
|
return Ok(TExpr {
|
|
ty: Type::U32,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
});
|
|
}
|
|
if is_numeric_primitive(&left.ty)
|
|
&& is_numeric_primitive(&right.ty)
|
|
&& left.ty != right.ty
|
|
{
|
|
return Err(numeric_operand_mismatch(file, expr, &left.ty, &right.ty));
|
|
}
|
|
expect_type(file, expr, &left.ty, &Type::I32)?;
|
|
expect_type(file, expr, &right.ty, &Type::I32)?;
|
|
Ok(TExpr {
|
|
ty: Type::I32,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
})
|
|
}
|
|
BinaryOp::Eq | BinaryOp::Lt | BinaryOp::Gt | BinaryOp::Le | BinaryOp::Ge => {
|
|
if matches!(op, BinaryOp::Eq)
|
|
&& (is_array_type(&left.ty) || is_array_type(&right.ty))
|
|
{
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"UnsupportedArrayEquality",
|
|
"array equality is not supported in the first-pass array feature",
|
|
)
|
|
.with_span(expr.span)
|
|
.hint("compare indexed array elements instead"));
|
|
}
|
|
if matches!(op, BinaryOp::Eq)
|
|
&& is_vec_i32_type(&left.ty)
|
|
&& is_vec_i32_type(&right.ty)
|
|
{
|
|
return Ok(TExpr {
|
|
ty: Type::Bool,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
});
|
|
}
|
|
if matches!(op, BinaryOp::Eq)
|
|
&& is_vec_i64_type(&left.ty)
|
|
&& is_vec_i64_type(&right.ty)
|
|
{
|
|
return Ok(TExpr {
|
|
ty: Type::Bool,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
});
|
|
}
|
|
if matches!(op, BinaryOp::Eq)
|
|
&& is_vec_f64_type(&left.ty)
|
|
&& is_vec_f64_type(&right.ty)
|
|
{
|
|
return Ok(TExpr {
|
|
ty: Type::Bool,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
});
|
|
}
|
|
if matches!(op, BinaryOp::Eq)
|
|
&& is_vec_bool_type(&left.ty)
|
|
&& is_vec_bool_type(&right.ty)
|
|
{
|
|
return Ok(TExpr {
|
|
ty: Type::Bool,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
});
|
|
}
|
|
if matches!(op, BinaryOp::Eq)
|
|
&& is_vec_string_type(&left.ty)
|
|
&& is_vec_string_type(&right.ty)
|
|
{
|
|
return Ok(TExpr {
|
|
ty: Type::Bool,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
});
|
|
}
|
|
if matches!(op, BinaryOp::Eq)
|
|
&& (is_vector_type(&left.ty) || is_vector_type(&right.ty))
|
|
{
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"UnsupportedVectorEquality",
|
|
"vector equality is supported only for `(vec i32)`, `(vec i64)`, `(vec f64)`, `(vec bool)`, or `(vec string)` values",
|
|
)
|
|
.with_span(expr.span)
|
|
.expected("(vec i32), (vec i64), (vec f64), (vec bool), or (vec string) on both sides")
|
|
.found(format!("{} and {}", left.ty, right.ty)));
|
|
}
|
|
if matches!(op, BinaryOp::Eq)
|
|
&& (is_option_result_type(&left.ty) || is_option_result_type(&right.ty))
|
|
{
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"UnsupportedOptionResultEquality",
|
|
"option/result equality is not supported in the current value-flow slice",
|
|
)
|
|
.with_span(expr.span)
|
|
.hint("observe the tag with `is_some`, `is_none`, `is_ok`, or `is_err`"));
|
|
}
|
|
if matches!(op, BinaryOp::Eq)
|
|
&& is_enum_type(&left.ty, enums)
|
|
&& left.ty == right.ty
|
|
{
|
|
if !enum_payload_equality_supported(&left.ty, enums) {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"UnsupportedEnumEquality",
|
|
"enum equality is supported only for payloadless enums and enums whose payload type is direct `i32`, `i64`, `u32`, `u64`, `f64`, `bool`, or `string`",
|
|
)
|
|
.with_span(expr.span)
|
|
.hint("match the enum and compare fields after extracting the struct payload"));
|
|
}
|
|
return Ok(TExpr {
|
|
ty: Type::Bool,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
});
|
|
}
|
|
if !matches!(op, BinaryOp::Eq)
|
|
&& (is_enum_type(&left.ty, enums) || is_enum_type(&right.ty, enums))
|
|
{
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"UnsupportedEnumOrdering",
|
|
"enum ordering comparisons are not supported",
|
|
)
|
|
.with_span(expr.span)
|
|
.hint("only enum equality with `=` is supported"));
|
|
}
|
|
if matches!(op, BinaryOp::Eq)
|
|
&& (is_enum_type(&left.ty, enums) || is_enum_type(&right.ty, enums))
|
|
{
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"EnumSubjectMismatch",
|
|
"enum equality requires both operands to have the same enum type",
|
|
)
|
|
.with_span(expr.span)
|
|
.expected(left.ty.to_string())
|
|
.found(right.ty.to_string()));
|
|
}
|
|
if matches!(op, BinaryOp::Eq)
|
|
&& left.ty == Type::String
|
|
&& right.ty == Type::String
|
|
{
|
|
return Ok(TExpr {
|
|
ty: Type::Bool,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
});
|
|
}
|
|
if matches!(op, BinaryOp::Eq) && left.ty == Type::Bool && right.ty == Type::Bool
|
|
{
|
|
return Ok(TExpr {
|
|
ty: Type::Bool,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
});
|
|
}
|
|
if left.ty == Type::F64 && right.ty == Type::F64 {
|
|
return Ok(TExpr {
|
|
ty: Type::Bool,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
});
|
|
}
|
|
if left.ty == Type::I64 && right.ty == Type::I64 {
|
|
return Ok(TExpr {
|
|
ty: Type::Bool,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
});
|
|
}
|
|
if left.ty == Type::U32 && right.ty == Type::U32 {
|
|
return Ok(TExpr {
|
|
ty: Type::Bool,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
});
|
|
}
|
|
if left.ty == Type::U64 && right.ty == Type::U64 {
|
|
return Ok(TExpr {
|
|
ty: Type::Bool,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
});
|
|
}
|
|
if left.ty == Type::U64 && right.ty == Type::U64 {
|
|
return Ok(TExpr {
|
|
ty: Type::Bool,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
});
|
|
}
|
|
if left.ty == Type::U32 && right.ty == Type::U32 {
|
|
return Ok(TExpr {
|
|
ty: Type::Bool,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
});
|
|
}
|
|
if is_numeric_primitive(&left.ty)
|
|
&& is_numeric_primitive(&right.ty)
|
|
&& left.ty != right.ty
|
|
{
|
|
return Err(numeric_operand_mismatch(file, expr, &left.ty, &right.ty));
|
|
}
|
|
expect_type(file, expr, &left.ty, &Type::I32)?;
|
|
expect_type(file, expr, &right.ty, &Type::I32)?;
|
|
Ok(TExpr {
|
|
ty: Type::Bool,
|
|
span: expr.span,
|
|
kind: TExprKind::Binary {
|
|
op: *op,
|
|
left: Box::new(left),
|
|
right: Box::new(right),
|
|
},
|
|
})
|
|
}
|
|
}
|
|
}
|
|
ExprKind::If {
|
|
condition,
|
|
then_expr,
|
|
else_expr,
|
|
} => {
|
|
let condition = check_expr(
|
|
file,
|
|
condition,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
)?;
|
|
if condition.ty != Type::Bool {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"IfConditionNotBool",
|
|
"`if` condition must be bool",
|
|
)
|
|
.with_span(condition.span)
|
|
.expected(Type::Bool.to_string())
|
|
.found(condition.ty.to_string()));
|
|
}
|
|
|
|
let then_expr = check_expr(
|
|
file,
|
|
then_expr,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
)?;
|
|
let else_expr = check_expr(
|
|
file,
|
|
else_expr,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
)?;
|
|
|
|
if then_expr.ty != else_expr.ty {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"IfBranchTypeMismatch",
|
|
"`if` branches must have the same type",
|
|
)
|
|
.with_span(expr.span)
|
|
.expected(then_expr.ty.to_string())
|
|
.found(else_expr.ty.to_string()));
|
|
}
|
|
|
|
Ok(TExpr {
|
|
ty: then_expr.ty.clone(),
|
|
span: expr.span,
|
|
kind: TExprKind::If {
|
|
condition: Box::new(condition),
|
|
then_expr: Box::new(then_expr),
|
|
else_expr: Box::new(else_expr),
|
|
},
|
|
})
|
|
}
|
|
ExprKind::Match { subject, arms } => check_match(
|
|
file,
|
|
expr,
|
|
subject,
|
|
arms,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
),
|
|
ExprKind::While { condition, body } => {
|
|
let condition = check_expr(
|
|
file,
|
|
condition,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
)?;
|
|
if condition.ty != Type::Bool {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"WhileConditionNotBool",
|
|
"`while` condition must be bool",
|
|
)
|
|
.with_span(condition.span)
|
|
.expected(Type::Bool.to_string())
|
|
.found(condition.ty.to_string()));
|
|
}
|
|
|
|
let mut checked_body = Vec::new();
|
|
for body_expr in body {
|
|
if matches!(body_expr.kind, ExprKind::Local { .. }) {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"LocalDeclarationInWhileBodyUnsupported",
|
|
"local declarations are not allowed inside `while` bodies",
|
|
)
|
|
.with_span(body_expr.span)
|
|
.hint("declare loop locals before the `while` form"));
|
|
}
|
|
|
|
if matches!(body_expr.kind, ExprKind::While { .. }) {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"NestedWhileUnsupported",
|
|
"nested `while` loops are not supported in first-pass loop bodies",
|
|
)
|
|
.with_span(body_expr.span)
|
|
.hint("keep first-pass loop bodies flat"));
|
|
}
|
|
|
|
let checked = check_expr(
|
|
file,
|
|
body_expr,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
)?;
|
|
if checked.ty != Type::Unit {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"WhileBodyFormNotUnit",
|
|
"`while` body forms must produce unit",
|
|
)
|
|
.with_span(checked.span)
|
|
.expected(Type::Unit.to_string())
|
|
.found(checked.ty.to_string()));
|
|
}
|
|
checked_body.push(checked);
|
|
}
|
|
|
|
Ok(TExpr {
|
|
ty: Type::Unit,
|
|
span: expr.span,
|
|
kind: TExprKind::While {
|
|
condition: Box::new(condition),
|
|
body: checked_body,
|
|
},
|
|
})
|
|
}
|
|
ExprKind::Unsafe { body } => {
|
|
check_unsafe_block(file, expr, body, locals, functions, structs, enums)
|
|
}
|
|
ExprKind::Call {
|
|
name,
|
|
name_span,
|
|
args,
|
|
} => {
|
|
if unsafe_ops::is_reserved_head(name) {
|
|
return Err(unsafe_operation_diagnostic(
|
|
file,
|
|
expr.span,
|
|
*name_span,
|
|
name,
|
|
unsafe_context,
|
|
));
|
|
}
|
|
|
|
if std_runtime::is_standard_path(name) && !std_runtime::is_promoted_source_name(name) {
|
|
return Err(std_runtime::unsupported_standard_library_call(
|
|
file, *name_span, name,
|
|
));
|
|
}
|
|
|
|
let sig = functions.get(name).ok_or_else(|| {
|
|
Diagnostic::new(
|
|
file,
|
|
"UnknownFunction",
|
|
format!("unknown function `{}`", name),
|
|
)
|
|
.with_span(expr.span)
|
|
})?;
|
|
|
|
if sig.foreign && !unsafe_context {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"UnsafeRequired",
|
|
format!("C import `{}` requires an `unsafe` block", name),
|
|
)
|
|
.with_span(*name_span)
|
|
.hint("wrap the call in `(unsafe ...)`")
|
|
.related("C import call head", *name_span));
|
|
}
|
|
|
|
if args.len() != sig.params.len() {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"ArityMismatch",
|
|
format!("function `{}` called with wrong number of arguments", name),
|
|
)
|
|
.with_span(expr.span)
|
|
.expected(sig.params.len().to_string())
|
|
.found(args.len().to_string()));
|
|
}
|
|
|
|
let mut checked_args = Vec::new();
|
|
|
|
for (arg, expected) in args.iter().zip(&sig.params) {
|
|
let checked =
|
|
check_expr(file, arg, locals, functions, structs, enums, unsafe_context)?;
|
|
let runtime_symbol = std_runtime::runtime_symbol(name).unwrap_or(name);
|
|
if matches!(
|
|
runtime_symbol,
|
|
"print_i32"
|
|
| "print_i64"
|
|
| "print_u32"
|
|
| "print_u64"
|
|
| "print_f64"
|
|
| "print_bool"
|
|
) && is_array_type(&checked.ty)
|
|
{
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"UnsupportedArrayPrint",
|
|
"array printing is not supported in the first-pass array feature",
|
|
)
|
|
.with_span(arg.span)
|
|
.hint("index one array element and print that value instead"));
|
|
}
|
|
if matches!(
|
|
runtime_symbol,
|
|
"print_i32" | "print_i64" | "print_u32" | "print_u64"
|
|
) && is_option_result_type(&checked.ty)
|
|
{
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"UnsupportedOptionResultPrint",
|
|
"option/result printing is not supported in the current value-flow slice",
|
|
)
|
|
.with_span(arg.span)
|
|
.hint("observe the tag with `is_some`, `is_none`, `is_ok`, or `is_err`"));
|
|
}
|
|
if matches!(
|
|
runtime_symbol,
|
|
"print_i32" | "print_i64" | "print_u32" | "print_u64"
|
|
) && is_enum_type(&checked.ty, enums)
|
|
{
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"UnsupportedEnumPrint",
|
|
"enum printing is not supported",
|
|
)
|
|
.with_span(arg.span)
|
|
.hint("compare enum values with `=` or match on variants"));
|
|
}
|
|
if &checked.ty != expected {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"TypeMismatch",
|
|
format!("cannot call `{}` with argument of wrong type", name),
|
|
)
|
|
.with_span(arg.span)
|
|
.expected(expected.to_string())
|
|
.found(checked.ty.to_string()));
|
|
}
|
|
if runtime_symbol == "print_string" {
|
|
check_print_string_literal(file, arg.span, &checked)?;
|
|
}
|
|
checked_args.push(checked);
|
|
}
|
|
|
|
Ok(TExpr {
|
|
ty: sig.return_type.clone(),
|
|
span: expr.span,
|
|
kind: TExprKind::Call {
|
|
name: name.clone(),
|
|
args: checked_args,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
fn check_print_string_literal(file: &str, span: Span, value: &TExpr) -> Result<(), Diagnostic> {
|
|
let TExprKind::String(value) = &value.kind else {
|
|
return Ok(());
|
|
};
|
|
|
|
if value.bytes().any(|byte| byte == 0 || !byte.is_ascii()) {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"UnsupportedStringLiteral",
|
|
"string literal contains bytes outside the first runtime string slice",
|
|
)
|
|
.with_span(span)
|
|
.expected("ASCII string literal without embedded NUL")
|
|
.found("unsupported string literal")
|
|
.hint("use ASCII text plus the current `\\n`, `\\t`, `\\\"`, and `\\\\` escapes"));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn numeric_operand_mismatch(file: &str, expr: &Expr, left: &Type, right: &Type) -> Diagnostic {
|
|
Diagnostic::new(
|
|
file,
|
|
"TypeMismatch",
|
|
"numeric operands must have the same primitive type",
|
|
)
|
|
.with_span(expr.span)
|
|
.expected("i32 with i32, i64 with i64, u32 with u32, u64 with u64, or f64 with f64")
|
|
.found(format!("{} and {}", left, right))
|
|
.hint("mixed i32/i64/u32/u64/f64 arithmetic and comparison are deferred")
|
|
}
|
|
|
|
fn is_numeric_primitive(ty: &Type) -> bool {
|
|
matches!(
|
|
ty,
|
|
Type::I32 | Type::I64 | Type::U32 | Type::U64 | Type::F64
|
|
)
|
|
}
|
|
|
|
enum MatchFamily {
|
|
Option {
|
|
payload_ty: Type,
|
|
},
|
|
Result {
|
|
ok_ty: Type,
|
|
err_ty: Type,
|
|
},
|
|
Enum {
|
|
name: String,
|
|
variants: Vec<EnumVariantSig>,
|
|
},
|
|
}
|
|
|
|
impl MatchFamily {
|
|
fn allows(&self, pattern: &MatchPatternKind) -> bool {
|
|
match self {
|
|
Self::Option { .. } => {
|
|
matches!(pattern, MatchPatternKind::Some | MatchPatternKind::None)
|
|
}
|
|
Self::Result { .. } => matches!(pattern, MatchPatternKind::Ok | MatchPatternKind::Err),
|
|
Self::Enum { name, variants } => matches!(
|
|
pattern,
|
|
MatchPatternKind::EnumVariant { enum_name, variant }
|
|
if enum_name == name && variants.iter().any(|candidate| &candidate.name == variant)
|
|
),
|
|
}
|
|
}
|
|
|
|
fn required(&self) -> Vec<MatchPatternKind> {
|
|
match self {
|
|
Self::Option { .. } => vec![MatchPatternKind::Some, MatchPatternKind::None],
|
|
Self::Result { .. } => vec![MatchPatternKind::Ok, MatchPatternKind::Err],
|
|
Self::Enum { name, variants } => variants
|
|
.iter()
|
|
.map(|variant| MatchPatternKind::EnumVariant {
|
|
enum_name: name.clone(),
|
|
variant: variant.name.clone(),
|
|
})
|
|
.collect(),
|
|
}
|
|
}
|
|
|
|
fn subject_type(&self) -> String {
|
|
match self {
|
|
Self::Option { payload_ty } => format!("(option {})", payload_ty),
|
|
Self::Result { ok_ty, err_ty } => format!("(result {} {})", ok_ty, err_ty),
|
|
Self::Enum { name, .. } => name.clone(),
|
|
}
|
|
}
|
|
|
|
fn binding_type(&self, pattern: &MatchPatternKind) -> Option<Type> {
|
|
match (self, pattern) {
|
|
(Self::Option { payload_ty }, MatchPatternKind::Some) => Some(payload_ty.clone()),
|
|
(Self::Result { ok_ty, .. }, MatchPatternKind::Ok) => Some(ok_ty.clone()),
|
|
(Self::Result { err_ty, .. }, MatchPatternKind::Err) => Some(err_ty.clone()),
|
|
(Self::Enum { variants, .. }, MatchPatternKind::EnumVariant { variant, .. }) => {
|
|
variants
|
|
.iter()
|
|
.find(|candidate| &candidate.name == variant)
|
|
.and_then(|candidate| candidate.payload_ty.clone())
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn discriminant(&self, pattern: &MatchPatternKind) -> Option<i32> {
|
|
match (self, pattern) {
|
|
(Self::Enum { variants, .. }, MatchPatternKind::EnumVariant { variant, .. }) => {
|
|
variants
|
|
.iter()
|
|
.position(|candidate| &candidate.name == variant)
|
|
.map(|index| index as i32)
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn pattern_requires_binding(&self, pattern: &MatchPatternKind) -> bool {
|
|
self.binding_type(pattern).is_some()
|
|
}
|
|
}
|
|
|
|
fn check_match(
|
|
file: &str,
|
|
expr: &Expr,
|
|
subject: &Expr,
|
|
arms: &[MatchArm],
|
|
locals: &HashMap<String, Binding>,
|
|
functions: &HashMap<String, FnSig>,
|
|
structs: &HashMap<String, StructSig>,
|
|
enums: &HashMap<String, EnumSig>,
|
|
unsafe_context: bool,
|
|
) -> Result<TExpr, Diagnostic> {
|
|
if let Some(err) = match_subject_syntax_diagnostic(file, subject, locals, enums) {
|
|
return Err(err);
|
|
}
|
|
|
|
let subject = check_expr(
|
|
file,
|
|
subject,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
)?;
|
|
let family = match_subject_family(file, &subject, enums)?;
|
|
|
|
validate_match_arm_set(file, &family, arms, expr.span)?;
|
|
|
|
let mut checked_arms = Vec::new();
|
|
let mut expected_arm_ty: Option<(Type, Span)> = None;
|
|
|
|
for arm in arms {
|
|
let mut arm_locals = locals.clone();
|
|
if let Some(binding) = &arm.pattern.binding {
|
|
let binding_span = arm.pattern.binding_span.unwrap_or(arm.pattern.span);
|
|
if let Some(existing) = locals.get(binding) {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"MatchBindingCollision",
|
|
format!(
|
|
"match payload binding `{}` collides with a visible name",
|
|
binding
|
|
),
|
|
)
|
|
.with_span(binding_span)
|
|
.related("visible binding", existing.span));
|
|
}
|
|
|
|
if functions.contains_key(binding) || is_reserved_callable_name(binding) {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"MatchBindingCollision",
|
|
format!(
|
|
"match payload binding `{}` collides with a function or intrinsic",
|
|
binding
|
|
),
|
|
)
|
|
.with_span(binding_span)
|
|
.hint("choose a payload name distinct from visible callables"));
|
|
}
|
|
|
|
if structs.contains_key(binding) {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"MatchBindingCollision",
|
|
format!("match payload binding `{}` collides with a struct", binding),
|
|
)
|
|
.with_span(binding_span)
|
|
.hint("choose a payload name distinct from visible structs"));
|
|
}
|
|
|
|
if let Some(ty) = family.binding_type(&arm.pattern.kind) {
|
|
arm_locals.insert(
|
|
binding.clone(),
|
|
Binding {
|
|
ty,
|
|
mutable: false,
|
|
kind: BindingKind::MatchPayload,
|
|
span: binding_span,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
let mut arm_errors = Vec::new();
|
|
let checked_body = check_body(
|
|
file,
|
|
&arm.body,
|
|
arm_locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
&mut arm_errors,
|
|
);
|
|
if let Some(err) = arm_errors.into_iter().next() {
|
|
return Err(err);
|
|
}
|
|
|
|
let Some(last) = checked_body.last() else {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"MalformedMatchPattern",
|
|
"match arm must contain at least one body expression",
|
|
)
|
|
.with_span(arm.span));
|
|
};
|
|
|
|
match &expected_arm_ty {
|
|
Some((expected, expected_span)) if last.ty != *expected => {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"MatchArmTypeMismatch",
|
|
"match arm final expressions must have the same type",
|
|
)
|
|
.with_span(last.span)
|
|
.expected(expected.to_string())
|
|
.found(last.ty.to_string())
|
|
.related("first arm result type", *expected_span));
|
|
}
|
|
None => expected_arm_ty = Some((last.ty.clone(), last.span)),
|
|
_ => {}
|
|
}
|
|
|
|
checked_arms.push(TMatchArm {
|
|
pattern: arm.pattern.kind.clone(),
|
|
binding: arm.pattern.binding.clone(),
|
|
discriminant: family.discriminant(&arm.pattern.kind),
|
|
body: checked_body,
|
|
});
|
|
}
|
|
|
|
let ty = expected_arm_ty.map(|(ty, _)| ty).unwrap_or(Type::Unit);
|
|
|
|
Ok(TExpr {
|
|
ty,
|
|
span: expr.span,
|
|
kind: TExprKind::Match {
|
|
subject: Box::new(subject),
|
|
arms: checked_arms,
|
|
},
|
|
})
|
|
}
|
|
|
|
fn match_subject_syntax_diagnostic(
|
|
file: &str,
|
|
subject: &Expr,
|
|
locals: &HashMap<String, Binding>,
|
|
enums: &HashMap<String, EnumSig>,
|
|
) -> Option<Diagnostic> {
|
|
match &subject.kind {
|
|
ExprKind::Var(name) => {
|
|
locals.get(name)?;
|
|
None
|
|
}
|
|
ExprKind::Set {
|
|
name, name_span, ..
|
|
} => {
|
|
let binding = locals.get(name)?;
|
|
if is_option_result_type(&binding.ty) {
|
|
Some(
|
|
Diagnostic::new(
|
|
file,
|
|
"UnsupportedMatchMutation",
|
|
"match subject cannot be an option/result assignment",
|
|
)
|
|
.with_span(*name_span)
|
|
.hint("evaluate and match an immutable option/result value"),
|
|
)
|
|
} else if is_enum_type(&binding.ty, enums) {
|
|
Some(
|
|
Diagnostic::new(
|
|
file,
|
|
"EnumLocalMutationUnsupported",
|
|
"match subject cannot be an enum assignment",
|
|
)
|
|
.with_span(*name_span)
|
|
.hint("evaluate and match an immutable enum value"),
|
|
)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
ExprKind::ArrayInit {
|
|
elem_ty,
|
|
elem_ty_span,
|
|
..
|
|
} if contains_enum_type(elem_ty, enums) => Some(
|
|
Diagnostic::new(
|
|
file,
|
|
"UnsupportedEnumContainer",
|
|
"match does not support enum values in array containers",
|
|
)
|
|
.with_span(*elem_ty_span)
|
|
.hint("match a direct enum value"),
|
|
),
|
|
ExprKind::ArrayInit {
|
|
elem_ty,
|
|
elem_ty_span,
|
|
..
|
|
} if contains_option_result_type(elem_ty) => Some(
|
|
Diagnostic::new(
|
|
file,
|
|
"UnsupportedMatchContainer",
|
|
"match does not support option/result values in array containers",
|
|
)
|
|
.with_span(*elem_ty_span)
|
|
.hint(
|
|
"match a direct `(option i32)`, `(option i64)`, `(option u32)`, `(option u64)`, `(option f64)`, `(option bool)`, `(option string)`, `(result i32 i32)`, `(result i64 i32)`, `(result u32 i32)`, `(result u64 i32)`, `(result f64 i32)`, `(result bool i32)`, or `(result string i32)` value",
|
|
),
|
|
),
|
|
ExprKind::OptionSome {
|
|
payload_ty,
|
|
payload_ty_span,
|
|
..
|
|
}
|
|
| ExprKind::OptionNone {
|
|
payload_ty,
|
|
payload_ty_span,
|
|
} if !option_payload_type_supported(payload_ty) => Some(unsupported_match_payload_type(
|
|
file,
|
|
*payload_ty_span,
|
|
payload_ty,
|
|
)),
|
|
ExprKind::ResultOk {
|
|
ok_ty,
|
|
ok_ty_span,
|
|
err_ty,
|
|
err_ty_span,
|
|
..
|
|
}
|
|
| ExprKind::ResultErr {
|
|
ok_ty,
|
|
ok_ty_span,
|
|
err_ty,
|
|
err_ty_span,
|
|
..
|
|
} => check_result_payload_types(file, ok_ty, *ok_ty_span, err_ty, *err_ty_span).err(),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn match_subject_family(
|
|
file: &str,
|
|
subject: &TExpr,
|
|
enums: &HashMap<String, EnumSig>,
|
|
) -> Result<MatchFamily, Diagnostic> {
|
|
match &subject.ty {
|
|
Type::Option(inner) if option_payload_type_supported(inner) => Ok(MatchFamily::Option {
|
|
payload_ty: (**inner).clone(),
|
|
}),
|
|
Type::Result(ok, err) if result_match_payload_types_supported(ok, err) => {
|
|
Ok(MatchFamily::Result {
|
|
ok_ty: (**ok).clone(),
|
|
err_ty: (**err).clone(),
|
|
})
|
|
}
|
|
Type::Named(name) if enums.contains_key(name) => {
|
|
let sig = enums.get(name).expect("enum checked above");
|
|
Ok(MatchFamily::Enum {
|
|
name: name.clone(),
|
|
variants: sig.variants.clone(),
|
|
})
|
|
}
|
|
Type::Option(inner) => Err(unsupported_match_payload_type(file, subject.span, inner)),
|
|
Type::Result(ok, err) => {
|
|
if **err != Type::I32 {
|
|
Err(unsupported_match_payload_type(file, subject.span, err))
|
|
} else {
|
|
Err(unsupported_match_payload_type(file, subject.span, ok))
|
|
}
|
|
}
|
|
Type::Array(inner, _) if contains_option_result_type(inner) => Err(Diagnostic::new(
|
|
file,
|
|
"UnsupportedMatchContainer",
|
|
"match does not support option/result values in array containers",
|
|
)
|
|
.with_span(subject.span)
|
|
.hint("match a direct `(option i32)`, `(option i64)`, `(option u32)`, `(option u64)`, `(option f64)`, `(option bool)`, `(option string)`, `(result i32 i32)`, `(result i64 i32)`, `(result u32 i32)`, `(result u64 i32)`, `(result f64 i32)`, `(result bool i32)`, or `(result string i32)` value")),
|
|
Type::Array(inner, _) if contains_enum_type(inner, enums) => Err(Diagnostic::new(
|
|
file,
|
|
"UnsupportedEnumContainer",
|
|
"match does not support enum values in array containers",
|
|
)
|
|
.with_span(subject.span)
|
|
.hint("match a direct enum value")),
|
|
_ => Err(Diagnostic::new(
|
|
file,
|
|
"MatchSubjectTypeMismatch",
|
|
"match subject must be `(option i32)`, `(option i64)`, `(option u32)`, `(option u64)`, `(option f64)`, `(option bool)`, `(option string)`, `(result i32 i32)`, `(result i64 i32)`, `(result u32 i32)`, `(result u64 i32)`, `(result f64 i32)`, `(result bool i32)`, `(result string i32)`, or a known enum",
|
|
)
|
|
.with_span(subject.span)
|
|
.expected("(option i32), (option i64), (option u32), (option u64), (option f64), (option bool), (option string), (result i32 i32), (result i64 i32), (result u32 i32), (result u64 i32), (result f64 i32), (result bool i32), (result string i32), or known enum")
|
|
.found(subject.ty.to_string())),
|
|
}
|
|
}
|
|
|
|
fn validate_match_arm_set(
|
|
file: &str,
|
|
family: &MatchFamily,
|
|
arms: &[MatchArm],
|
|
span: Span,
|
|
) -> Result<(), Diagnostic> {
|
|
let mut seen = HashMap::new();
|
|
|
|
for arm in arms {
|
|
let pattern = &arm.pattern.kind;
|
|
if !family.allows(pattern) {
|
|
let code = if matches!(pattern, MatchPatternKind::EnumVariant { .. }) {
|
|
"InvalidEnumMatchArm"
|
|
} else {
|
|
"MalformedMatchPattern"
|
|
};
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
code,
|
|
format!(
|
|
"`{}` arm is not valid for `{}` match",
|
|
lower::match_pattern_name(pattern),
|
|
family.subject_type()
|
|
),
|
|
)
|
|
.with_span(arm.pattern.span));
|
|
}
|
|
|
|
let requires_binding = family.pattern_requires_binding(pattern);
|
|
if requires_binding && arm.pattern.binding.is_none() {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"InvalidEnumMatchArm",
|
|
format!(
|
|
"`{}` match arm must bind its payload",
|
|
lower::match_pattern_name(pattern)
|
|
),
|
|
)
|
|
.with_span(arm.pattern.span)
|
|
.expected(format!("({} binding)", lower::match_pattern_name(pattern))));
|
|
}
|
|
if !requires_binding && arm.pattern.binding.is_some() {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"InvalidEnumMatchArm",
|
|
format!(
|
|
"`{}` match arm does not have a payload to bind",
|
|
lower::match_pattern_name(pattern)
|
|
),
|
|
)
|
|
.with_span(arm.pattern.span)
|
|
.expected(format!("({})", lower::match_pattern_name(pattern))));
|
|
}
|
|
|
|
if let Some(original) = seen.insert(pattern.clone(), arm.pattern.span) {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"DuplicateMatchArm",
|
|
format!(
|
|
"duplicate `{}` match arm",
|
|
lower::match_pattern_name(pattern)
|
|
),
|
|
)
|
|
.with_span(arm.pattern.span)
|
|
.related("original match arm", original));
|
|
}
|
|
}
|
|
|
|
let required = family.required();
|
|
let missing = required
|
|
.iter()
|
|
.filter(|pattern| !seen.contains_key(*pattern))
|
|
.map(lower::match_pattern_name)
|
|
.collect::<Vec<_>>();
|
|
|
|
if !missing.is_empty() {
|
|
let found = required
|
|
.iter()
|
|
.filter(|pattern| seen.contains_key(*pattern))
|
|
.map(lower::match_pattern_name)
|
|
.collect::<Vec<_>>();
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"NonExhaustiveMatch",
|
|
format!(
|
|
"match is missing required arm(s): `{}`",
|
|
missing.join("`, `")
|
|
),
|
|
)
|
|
.with_span(span)
|
|
.expected(
|
|
required
|
|
.iter()
|
|
.map(lower::match_pattern_name)
|
|
.collect::<Vec<_>>()
|
|
.join(" and "),
|
|
)
|
|
.found(if found.is_empty() {
|
|
"no valid arms".to_string()
|
|
} else {
|
|
found.join(", ")
|
|
}));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn contains_option_result_type(ty: &Type) -> bool {
|
|
match ty {
|
|
Type::Option(_) | Type::Result(_, _) => true,
|
|
Type::Ptr(inner) | Type::Array(inner, _) | Type::Vec(inner) | Type::Slice(inner) => {
|
|
contains_option_result_type(inner)
|
|
}
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
fn contains_enum_type(ty: &Type, enums: &HashMap<String, EnumSig>) -> bool {
|
|
match ty {
|
|
Type::Named(name) => enums.contains_key(name),
|
|
Type::Ptr(inner) | Type::Array(inner, _) | Type::Vec(inner) | Type::Slice(inner) => {
|
|
contains_enum_type(inner, enums)
|
|
}
|
|
Type::Option(inner) => contains_enum_type(inner, enums),
|
|
Type::Result(ok, err) => contains_enum_type(ok, enums) || contains_enum_type(err, enums),
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
fn supported_struct_field_message() -> &'static str {
|
|
"released struct fields support direct `i32`, `i64`, `f64`, `bool`, `string`, direct fixed arrays of those element families, direct enum fields, current concrete vec/option/result fields, and current non-recursive struct fields"
|
|
}
|
|
|
|
fn supported_struct_field_expected() -> &'static str {
|
|
"direct `i32`, `i64`, `f64`, `bool`, `string`, direct fixed array of those element families, direct enum field, current concrete vec/option/result field, or current non-recursive struct field"
|
|
}
|
|
|
|
fn validate_struct_field_cycles(
|
|
file: &str,
|
|
structs: &HashMap<String, StructSig>,
|
|
) -> Vec<Diagnostic> {
|
|
fn visit(
|
|
file: &str,
|
|
name: &str,
|
|
structs: &HashMap<String, StructSig>,
|
|
stack: &mut Vec<String>,
|
|
visited: &mut HashSet<String>,
|
|
errors: &mut Vec<Diagnostic>,
|
|
) {
|
|
if !visited.insert(name.to_string()) {
|
|
return;
|
|
}
|
|
|
|
stack.push(name.to_string());
|
|
|
|
if let Some(sig) = structs.get(name) {
|
|
for field in &sig.fields {
|
|
let Type::Named(target) = &field.ty else {
|
|
continue;
|
|
};
|
|
let Some(target_sig) = structs.get(target) else {
|
|
continue;
|
|
};
|
|
|
|
if let Some(index) = stack.iter().position(|entry| entry == target) {
|
|
let cycle = stack[index..].join(" -> ");
|
|
errors.push(
|
|
Diagnostic::new(
|
|
file,
|
|
"RecursiveStructFieldUnsupported",
|
|
format!(
|
|
"recursive struct fields are not supported (`{} -> {}`)",
|
|
cycle, target
|
|
),
|
|
)
|
|
.with_span(field.ty_span)
|
|
.related("recursive struct declaration", target_sig.span)
|
|
.hint("flatten the struct graph or remove the recursive field"),
|
|
);
|
|
continue;
|
|
}
|
|
|
|
visit(file, target, structs, stack, visited, errors);
|
|
}
|
|
}
|
|
|
|
stack.pop();
|
|
}
|
|
|
|
let mut errors = Vec::new();
|
|
let mut visited = HashSet::new();
|
|
let mut names = structs.keys().cloned().collect::<Vec<_>>();
|
|
names.sort();
|
|
for name in names {
|
|
let mut stack = Vec::new();
|
|
visit(file, &name, structs, &mut stack, &mut visited, &mut errors);
|
|
}
|
|
errors
|
|
}
|
|
|
|
fn unsupported_enum_container(file: &str, span: Span, ty: &Type) -> Diagnostic {
|
|
Diagnostic::new(
|
|
file,
|
|
"UnsupportedEnumContainer",
|
|
"enum values in container types are not supported",
|
|
)
|
|
.with_span(span)
|
|
.expected("direct enum type")
|
|
.found(ty.to_string())
|
|
.hint("use enum values directly in immutable locals, parameters, returns, calls, equality, or match")
|
|
}
|
|
|
|
fn unsupported_match_payload_type(file: &str, span: Span, ty: &Type) -> Diagnostic {
|
|
Diagnostic::new(
|
|
file,
|
|
"UnsupportedMatchPayloadType",
|
|
"match supports only `(option i32)`, `(option i64)`, `(option f64)`, `(option bool)`, `(option string)`, `(result i32 i32)`, `(result i64 i32)`, `(result f64 i32)`, `(result bool i32)`, and `(result string i32)` payloads",
|
|
)
|
|
.with_span(span)
|
|
.expected("i32 option payloads, i64 option payloads, f64 option payloads, bool option payloads, string option payloads, i32/i32 result payloads, i64/i32 result payloads, f64/i32 result payloads, bool/i32 result payloads, or string/i32 result payloads")
|
|
.found(ty.to_string())
|
|
}
|
|
|
|
fn check_unsafe_block(
|
|
file: &str,
|
|
expr: &Expr,
|
|
body: &[Expr],
|
|
locals: &HashMap<String, Binding>,
|
|
functions: &HashMap<String, FnSig>,
|
|
structs: &HashMap<String, StructSig>,
|
|
enums: &HashMap<String, EnumSig>,
|
|
) -> Result<TExpr, Diagnostic> {
|
|
if body.is_empty() {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"MalformedUnsafeForm",
|
|
"`unsafe` block must contain a final expression",
|
|
)
|
|
.with_span(expr.span)
|
|
.expected("(unsafe body-form... final-expression)"));
|
|
}
|
|
|
|
let mut errors = Vec::new();
|
|
let checked_body = check_body(
|
|
file,
|
|
body,
|
|
locals.clone(),
|
|
functions,
|
|
structs,
|
|
enums,
|
|
true,
|
|
&mut errors,
|
|
);
|
|
|
|
if let Some(err) = errors.into_iter().next() {
|
|
return Err(err);
|
|
}
|
|
|
|
let Some(last) = checked_body.last() else {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"MalformedUnsafeForm",
|
|
"`unsafe` block must contain a final expression",
|
|
)
|
|
.with_span(expr.span)
|
|
.expected("(unsafe body-form... final-expression)"));
|
|
};
|
|
|
|
Ok(TExpr {
|
|
ty: last.ty.clone(),
|
|
span: expr.span,
|
|
kind: TExprKind::Unsafe { body: checked_body },
|
|
})
|
|
}
|
|
|
|
fn unsafe_operation_diagnostic(
|
|
file: &str,
|
|
span: Span,
|
|
name_span: Span,
|
|
name: &str,
|
|
unsafe_context: bool,
|
|
) -> Diagnostic {
|
|
if unsafe_context {
|
|
Diagnostic::new(
|
|
file,
|
|
"UnsupportedUnsafeOperation",
|
|
format!(
|
|
"unsafe operation `{}` is outside the v1.6 unsafe contract",
|
|
name
|
|
),
|
|
)
|
|
.with_span(span)
|
|
.hint("raw memory operations are not supported by v1.6 unsafe blocks")
|
|
.related("unsafe operation head", name_span)
|
|
} else {
|
|
Diagnostic::new(
|
|
file,
|
|
"UnsafeRequired",
|
|
format!("unsafe operation `{}` requires an `unsafe` block", name),
|
|
)
|
|
.with_span(span)
|
|
.hint("wrap the operation in `(unsafe ...)`")
|
|
.related("unsafe operation head", name_span)
|
|
}
|
|
}
|
|
|
|
fn check_struct_init(
|
|
file: &str,
|
|
expr: &Expr,
|
|
name: &str,
|
|
name_span: Span,
|
|
fields: &[StructInitField],
|
|
locals: &HashMap<String, Binding>,
|
|
functions: &HashMap<String, FnSig>,
|
|
structs: &HashMap<String, StructSig>,
|
|
enums: &HashMap<String, EnumSig>,
|
|
unsafe_context: bool,
|
|
) -> Result<TExpr, Diagnostic> {
|
|
let sig = structs.get(name).ok_or_else(|| {
|
|
Diagnostic::new(file, "UnknownStruct", format!("unknown struct `{}`", name))
|
|
.with_span(name_span)
|
|
})?;
|
|
|
|
let mut seen = HashMap::new();
|
|
let mut checked_by_name = HashMap::new();
|
|
|
|
for (position, field) in fields.iter().enumerate() {
|
|
match seen.entry(field.name.clone()) {
|
|
Entry::Vacant(entry) => {
|
|
entry.insert(field.name_span);
|
|
}
|
|
Entry::Occupied(entry) => {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"DuplicateStructConstructorField",
|
|
format!("constructor for `{}` repeats field `{}`", name, field.name),
|
|
)
|
|
.with_span(field.name_span)
|
|
.related("original constructor field", *entry.get()));
|
|
}
|
|
}
|
|
|
|
let Some(index) = sig.fields_by_name.get(&field.name) else {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"UnknownStructField",
|
|
format!("struct `{}` has no field `{}`", name, field.name),
|
|
)
|
|
.with_span(field.name_span));
|
|
};
|
|
|
|
let expected_field = &sig.fields[position];
|
|
if expected_field.name != field.name {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"StructConstructorFieldOrderMismatch",
|
|
format!("constructor for `{}` lists fields out of order", name),
|
|
)
|
|
.with_span(field.name_span)
|
|
.expected(expected_field.name.clone())
|
|
.found(field.name.clone()));
|
|
}
|
|
|
|
let expected = &sig.fields[*index].ty;
|
|
let checked = check_expr(
|
|
file,
|
|
&field.expr,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
)?;
|
|
if &checked.ty != expected {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"TypeMismatch",
|
|
format!("field `{}` initializer has the wrong type", field.name),
|
|
)
|
|
.with_span(checked.span)
|
|
.expected(expected.to_string())
|
|
.found(checked.ty.to_string()));
|
|
}
|
|
|
|
checked_by_name.insert(field.name.clone(), checked);
|
|
}
|
|
|
|
for field in &sig.fields {
|
|
if !checked_by_name.contains_key(&field.name) {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"MissingStructField",
|
|
format!(
|
|
"constructor for `{}` is missing field `{}`",
|
|
name, field.name
|
|
),
|
|
)
|
|
.with_span(expr.span)
|
|
.expected(format!("field `{}`", field.name)));
|
|
}
|
|
}
|
|
|
|
let fields = sig
|
|
.fields
|
|
.iter()
|
|
.map(|field| {
|
|
(
|
|
field.name.clone(),
|
|
checked_by_name
|
|
.get(&field.name)
|
|
.expect("checked constructor has all declared fields")
|
|
.clone(),
|
|
)
|
|
})
|
|
.collect();
|
|
|
|
Ok(TExpr {
|
|
ty: Type::Named(name.to_string()),
|
|
span: expr.span,
|
|
kind: TExprKind::StructInit {
|
|
name: name.to_string(),
|
|
fields,
|
|
},
|
|
})
|
|
}
|
|
|
|
fn check_enum_variant(
|
|
file: &str,
|
|
expr: &Expr,
|
|
enum_name: &str,
|
|
variant: &str,
|
|
name_span: Span,
|
|
args: &[Expr],
|
|
locals: &HashMap<String, Binding>,
|
|
functions: &HashMap<String, FnSig>,
|
|
structs: &HashMap<String, StructSig>,
|
|
enums: &HashMap<String, EnumSig>,
|
|
unsafe_context: bool,
|
|
) -> Result<TExpr, Diagnostic> {
|
|
let sig = enums.get(enum_name).ok_or_else(|| {
|
|
Diagnostic::new(
|
|
file,
|
|
"UnknownEnumConstructor",
|
|
format!("unknown enum `{}` in variant constructor", enum_name),
|
|
)
|
|
.with_span(name_span)
|
|
})?;
|
|
|
|
let Some(discriminant) = sig.variants_by_name.get(variant).copied() else {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"UnknownVariantConstructor",
|
|
format!("enum `{}` has no variant `{}`", enum_name, variant),
|
|
)
|
|
.with_span(name_span)
|
|
.expected(format!(
|
|
"one of {}",
|
|
sig.variants
|
|
.iter()
|
|
.map(|variant| variant.name.as_str())
|
|
.collect::<Vec<_>>()
|
|
.join(", ")
|
|
)));
|
|
};
|
|
let variant_sig = &sig.variants[discriminant];
|
|
let expected_arity = if variant_sig.payload_ty.is_some() {
|
|
1
|
|
} else {
|
|
0
|
|
};
|
|
if args.len() != expected_arity {
|
|
let message = if expected_arity == 0 {
|
|
format!(
|
|
"enum constructor `{}.{}` takes no arguments",
|
|
enum_name, variant
|
|
)
|
|
} else {
|
|
let payload_ty = variant_sig
|
|
.payload_ty
|
|
.as_ref()
|
|
.expect("payload arity without payload type");
|
|
format!(
|
|
"enum constructor `{}.{}` requires one {} payload argument",
|
|
enum_name, variant, payload_ty
|
|
)
|
|
};
|
|
return Err(Diagnostic::new(file, "VariantConstructorArity", message)
|
|
.with_span(expr.span)
|
|
.expected(expected_arity.to_string())
|
|
.found(args.len().to_string()));
|
|
}
|
|
|
|
let payload = if let Some(payload_ty) = &variant_sig.payload_ty {
|
|
let checked = check_expr(
|
|
file,
|
|
&args[0],
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
)?;
|
|
if &checked.ty != payload_ty {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"TypeMismatch",
|
|
format!(
|
|
"enum constructor `{}.{}` payload has wrong type",
|
|
enum_name, variant
|
|
),
|
|
)
|
|
.with_span(checked.span)
|
|
.expected(payload_ty.to_string())
|
|
.found(checked.ty.to_string()));
|
|
}
|
|
Some(Box::new(checked))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
Ok(TExpr {
|
|
ty: Type::Named(enum_name.to_string()),
|
|
span: expr.span,
|
|
kind: TExprKind::EnumVariant {
|
|
enum_name: enum_name.to_string(),
|
|
variant: variant.to_string(),
|
|
discriminant: discriminant as i32,
|
|
payload,
|
|
},
|
|
})
|
|
}
|
|
|
|
fn check_option_some(
|
|
file: &str,
|
|
expr: &Expr,
|
|
payload_ty: &Type,
|
|
payload_ty_span: Span,
|
|
value: &Expr,
|
|
locals: &HashMap<String, Binding>,
|
|
functions: &HashMap<String, FnSig>,
|
|
structs: &HashMap<String, StructSig>,
|
|
enums: &HashMap<String, EnumSig>,
|
|
unsafe_context: bool,
|
|
) -> Result<TExpr, Diagnostic> {
|
|
check_option_payload_type(file, payload_ty, payload_ty_span)?;
|
|
|
|
let value = check_expr(
|
|
file,
|
|
value,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
)?;
|
|
if value.ty != *payload_ty {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"TypeMismatch",
|
|
"option `some` value has the wrong type",
|
|
)
|
|
.with_span(value.span)
|
|
.expected(payload_ty.to_string())
|
|
.found(value.ty.to_string()));
|
|
}
|
|
|
|
Ok(TExpr {
|
|
ty: Type::Option(Box::new(payload_ty.clone())),
|
|
span: expr.span,
|
|
kind: TExprKind::OptionSome {
|
|
value: Box::new(value),
|
|
},
|
|
})
|
|
}
|
|
|
|
fn check_option_none(
|
|
file: &str,
|
|
expr: &Expr,
|
|
payload_ty: &Type,
|
|
payload_ty_span: Span,
|
|
) -> Result<TExpr, Diagnostic> {
|
|
check_option_payload_type(file, payload_ty, payload_ty_span)?;
|
|
|
|
Ok(TExpr {
|
|
ty: Type::Option(Box::new(payload_ty.clone())),
|
|
span: expr.span,
|
|
kind: TExprKind::OptionNone,
|
|
})
|
|
}
|
|
|
|
fn check_result_ok(
|
|
file: &str,
|
|
expr: &Expr,
|
|
ok_ty: &Type,
|
|
ok_ty_span: Span,
|
|
err_ty: &Type,
|
|
err_ty_span: Span,
|
|
value: &Expr,
|
|
locals: &HashMap<String, Binding>,
|
|
functions: &HashMap<String, FnSig>,
|
|
structs: &HashMap<String, StructSig>,
|
|
enums: &HashMap<String, EnumSig>,
|
|
unsafe_context: bool,
|
|
) -> Result<TExpr, Diagnostic> {
|
|
check_result_constructor_payload_types(file, ok_ty, ok_ty_span, err_ty, err_ty_span)?;
|
|
|
|
let value = check_expr(
|
|
file,
|
|
value,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
)?;
|
|
if value.ty != *ok_ty {
|
|
return Err(
|
|
Diagnostic::new(file, "TypeMismatch", "result `ok` value has the wrong type")
|
|
.with_span(value.span)
|
|
.expected(ok_ty.to_string())
|
|
.found(value.ty.to_string()),
|
|
);
|
|
}
|
|
|
|
Ok(TExpr {
|
|
ty: Type::Result(Box::new(ok_ty.clone()), Box::new(err_ty.clone())),
|
|
span: expr.span,
|
|
kind: TExprKind::ResultOk {
|
|
value: Box::new(value),
|
|
},
|
|
})
|
|
}
|
|
|
|
fn check_result_err(
|
|
file: &str,
|
|
expr: &Expr,
|
|
ok_ty: &Type,
|
|
ok_ty_span: Span,
|
|
err_ty: &Type,
|
|
err_ty_span: Span,
|
|
value: &Expr,
|
|
locals: &HashMap<String, Binding>,
|
|
functions: &HashMap<String, FnSig>,
|
|
structs: &HashMap<String, StructSig>,
|
|
enums: &HashMap<String, EnumSig>,
|
|
unsafe_context: bool,
|
|
) -> Result<TExpr, Diagnostic> {
|
|
check_result_constructor_payload_types(file, ok_ty, ok_ty_span, err_ty, err_ty_span)?;
|
|
|
|
let value = check_expr(
|
|
file,
|
|
value,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
)?;
|
|
if value.ty != *err_ty {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"TypeMismatch",
|
|
"result `err` value has the wrong type",
|
|
)
|
|
.with_span(value.span)
|
|
.expected(err_ty.to_string())
|
|
.found(value.ty.to_string()));
|
|
}
|
|
|
|
Ok(TExpr {
|
|
ty: Type::Result(Box::new(ok_ty.clone()), Box::new(err_ty.clone())),
|
|
span: expr.span,
|
|
kind: TExprKind::ResultErr {
|
|
value: Box::new(value),
|
|
},
|
|
})
|
|
}
|
|
|
|
fn check_option_observer(
|
|
file: &str,
|
|
expr: &Expr,
|
|
value: &Expr,
|
|
op: &str,
|
|
locals: &HashMap<String, Binding>,
|
|
functions: &HashMap<String, FnSig>,
|
|
structs: &HashMap<String, StructSig>,
|
|
enums: &HashMap<String, EnumSig>,
|
|
unsafe_context: bool,
|
|
) -> Result<TExpr, Diagnostic> {
|
|
let value = check_expr(
|
|
file,
|
|
value,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
)?;
|
|
if !matches!(value.ty, Type::Option(ref inner) if option_payload_type_supported(inner)) {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"OptionObservationTypeMismatch",
|
|
format!(
|
|
"`{}` requires an `(option i32)`, `(option i64)`, `(option f64)`, `(option bool)`, or `(option string)` value",
|
|
op
|
|
),
|
|
)
|
|
.with_span(value.span)
|
|
.expected("(option i32), (option i64), (option f64), (option bool), or (option string)")
|
|
.found(value.ty.to_string()));
|
|
}
|
|
|
|
let kind = match op {
|
|
"is_some" => TExprKind::OptionIsSome {
|
|
value: Box::new(value),
|
|
},
|
|
"is_none" => TExprKind::OptionIsNone {
|
|
value: Box::new(value),
|
|
},
|
|
_ => unreachable!("unknown option observer"),
|
|
};
|
|
|
|
Ok(TExpr {
|
|
ty: Type::Bool,
|
|
span: expr.span,
|
|
kind,
|
|
})
|
|
}
|
|
|
|
fn check_result_observer(
|
|
file: &str,
|
|
expr: &Expr,
|
|
value: &Expr,
|
|
op: &str,
|
|
locals: &HashMap<String, Binding>,
|
|
functions: &HashMap<String, FnSig>,
|
|
structs: &HashMap<String, StructSig>,
|
|
enums: &HashMap<String, EnumSig>,
|
|
unsafe_context: bool,
|
|
) -> Result<TExpr, Diagnostic> {
|
|
let value = check_expr(
|
|
file,
|
|
value,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
)?;
|
|
if !matches!(value.ty, Type::Result(ref ok, ref err) if result_payload_types_supported(ok, err))
|
|
{
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"ResultObservationTypeMismatch",
|
|
format!(
|
|
"`{}` requires a `(result i32 i32)`, `(result i64 i32)`, `(result f64 i32)`, `(result bool i32)`, or `(result string i32)` value",
|
|
op
|
|
),
|
|
)
|
|
.with_span(value.span)
|
|
.expected("(result i32 i32), (result i64 i32), (result f64 i32), (result bool i32), or (result string i32)")
|
|
.found(value.ty.to_string()));
|
|
}
|
|
|
|
let kind = match op {
|
|
"is_ok" | "std.result.is_ok" => TExprKind::ResultIsOk {
|
|
source_name: op.to_string(),
|
|
value: Box::new(value),
|
|
},
|
|
"is_err" | "std.result.is_err" => TExprKind::ResultIsErr {
|
|
source_name: op.to_string(),
|
|
value: Box::new(value),
|
|
},
|
|
_ => unreachable!("unknown result observer"),
|
|
};
|
|
|
|
Ok(TExpr {
|
|
ty: Type::Bool,
|
|
span: expr.span,
|
|
kind,
|
|
})
|
|
}
|
|
|
|
fn check_option_unwrap(
|
|
file: &str,
|
|
expr: &Expr,
|
|
value: &Expr,
|
|
locals: &HashMap<String, Binding>,
|
|
functions: &HashMap<String, FnSig>,
|
|
structs: &HashMap<String, StructSig>,
|
|
enums: &HashMap<String, EnumSig>,
|
|
unsafe_context: bool,
|
|
) -> Result<TExpr, Diagnostic> {
|
|
let value = check_expr(
|
|
file,
|
|
value,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
)?;
|
|
let Type::Option(inner) = &value.ty else {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"OptionUnwrapTypeMismatch",
|
|
"`unwrap_some` requires an `(option i32)`, `(option i64)`, `(option f64)`, `(option bool)`, or `(option string)` value",
|
|
)
|
|
.with_span(value.span)
|
|
.expected("(option i32), (option i64), (option f64), (option bool), or (option string)")
|
|
.found(value.ty.to_string()));
|
|
};
|
|
if !option_payload_type_supported(inner) {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"OptionUnwrapTypeMismatch",
|
|
"`unwrap_some` requires an `(option i32)`, `(option i64)`, `(option f64)`, `(option bool)`, or `(option string)` value",
|
|
)
|
|
.with_span(value.span)
|
|
.expected("(option i32), (option i64), (option f64), (option bool), or (option string)")
|
|
.found(value.ty.to_string()));
|
|
}
|
|
|
|
Ok(TExpr {
|
|
ty: (**inner).clone(),
|
|
span: expr.span,
|
|
kind: TExprKind::OptionUnwrapSome {
|
|
value: Box::new(value),
|
|
},
|
|
})
|
|
}
|
|
|
|
fn check_result_unwrap(
|
|
file: &str,
|
|
expr: &Expr,
|
|
value: &Expr,
|
|
op: &str,
|
|
locals: &HashMap<String, Binding>,
|
|
functions: &HashMap<String, FnSig>,
|
|
structs: &HashMap<String, StructSig>,
|
|
enums: &HashMap<String, EnumSig>,
|
|
unsafe_context: bool,
|
|
) -> Result<TExpr, Diagnostic> {
|
|
let value = check_expr(
|
|
file,
|
|
value,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
)?;
|
|
let (ok_ty, err_ty) = match &value.ty {
|
|
Type::Result(ok, err) if result_payload_types_supported(ok, err) => {
|
|
((**ok).clone(), (**err).clone())
|
|
}
|
|
_ => {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"ResultUnwrapTypeMismatch",
|
|
format!(
|
|
"`{}` requires a `(result i32 i32)`, `(result i64 i32)`, `(result f64 i32)`, `(result bool i32)`, or `(result string i32)` value",
|
|
op
|
|
),
|
|
)
|
|
.with_span(value.span)
|
|
.expected("(result i32 i32), (result i64 i32), (result f64 i32), (result bool i32), or (result string i32)")
|
|
.found(value.ty.to_string()));
|
|
}
|
|
};
|
|
|
|
let result_ty = match op {
|
|
"unwrap_ok" | "std.result.unwrap_ok" => ok_ty,
|
|
"unwrap_err" | "std.result.unwrap_err" => err_ty,
|
|
_ => unreachable!("unknown result payload access"),
|
|
};
|
|
|
|
let kind = match op {
|
|
"unwrap_ok" | "std.result.unwrap_ok" => TExprKind::ResultUnwrapOk {
|
|
source_name: op.to_string(),
|
|
value: Box::new(value),
|
|
},
|
|
"unwrap_err" | "std.result.unwrap_err" => TExprKind::ResultUnwrapErr {
|
|
source_name: op.to_string(),
|
|
value: Box::new(value),
|
|
},
|
|
_ => unreachable!("unknown result payload access"),
|
|
};
|
|
|
|
Ok(TExpr {
|
|
ty: result_ty,
|
|
span: expr.span,
|
|
kind,
|
|
})
|
|
}
|
|
|
|
fn check_option_result_type(file: &str, ty: &Type, span: Span) -> Result<(), Diagnostic> {
|
|
match ty {
|
|
Type::Option(inner) => check_option_payload_type(file, inner, span),
|
|
Type::Result(ok, err) => check_result_payload_types(file, ok, span, err, span),
|
|
_ => Ok(()),
|
|
}
|
|
}
|
|
|
|
fn check_option_payload_type(file: &str, ty: &Type, span: Span) -> Result<(), Diagnostic> {
|
|
if option_payload_type_supported(ty) {
|
|
Ok(())
|
|
} else {
|
|
Err(unsupported_option_payload_type(file, span, ty))
|
|
}
|
|
}
|
|
|
|
fn option_payload_type_supported(ty: &Type) -> bool {
|
|
matches!(
|
|
ty,
|
|
Type::I32 | Type::I64 | Type::U32 | Type::U64 | Type::F64 | Type::Bool | Type::String
|
|
)
|
|
}
|
|
|
|
fn check_result_payload_types(
|
|
file: &str,
|
|
ok_ty: &Type,
|
|
ok_ty_span: Span,
|
|
err_ty: &Type,
|
|
err_ty_span: Span,
|
|
) -> Result<(), Diagnostic> {
|
|
if result_payload_types_supported(ok_ty, err_ty) {
|
|
return Ok(());
|
|
}
|
|
|
|
if err_ty != &Type::I32 {
|
|
return Err(unsupported_result_payload_type(file, err_ty_span, err_ty));
|
|
}
|
|
|
|
Err(unsupported_result_payload_type(file, ok_ty_span, ok_ty))
|
|
}
|
|
|
|
fn result_payload_types_supported(ok_ty: &Type, err_ty: &Type) -> bool {
|
|
matches!(
|
|
(ok_ty, err_ty),
|
|
(Type::I32, Type::I32)
|
|
| (Type::I64, Type::I32)
|
|
| (Type::U32, Type::I32)
|
|
| (Type::U64, Type::I32)
|
|
| (Type::F64, Type::I32)
|
|
| (Type::Bool, Type::I32)
|
|
| (Type::String, Type::I32)
|
|
)
|
|
}
|
|
|
|
fn result_constructor_payload_types_supported(ok_ty: &Type, err_ty: &Type) -> bool {
|
|
result_payload_types_supported(ok_ty, err_ty)
|
|
}
|
|
|
|
fn result_match_payload_types_supported(ok_ty: &Type, err_ty: &Type) -> bool {
|
|
result_payload_types_supported(ok_ty, err_ty)
|
|
}
|
|
|
|
fn check_result_constructor_payload_types(
|
|
file: &str,
|
|
ok_ty: &Type,
|
|
ok_ty_span: Span,
|
|
err_ty: &Type,
|
|
err_ty_span: Span,
|
|
) -> Result<(), Diagnostic> {
|
|
if result_constructor_payload_types_supported(ok_ty, err_ty) {
|
|
return Ok(());
|
|
}
|
|
|
|
if err_ty != &Type::I32 {
|
|
return Err(unsupported_result_payload_type(file, err_ty_span, err_ty));
|
|
}
|
|
|
|
Err(unsupported_result_payload_type(file, ok_ty_span, ok_ty))
|
|
}
|
|
|
|
fn unsupported_option_payload_type(file: &str, span: Span, ty: &Type) -> Diagnostic {
|
|
Diagnostic::new(
|
|
file,
|
|
"UnsupportedOptionPayloadType",
|
|
"first-pass options support only `i32`, `i64`, `u32`, `u64`, `f64`, `bool`, and `string` payloads",
|
|
)
|
|
.with_span(span)
|
|
.expected("i32, i64, u32, u64, f64, bool, or string")
|
|
.found(ty.to_string())
|
|
}
|
|
|
|
fn unsupported_result_payload_type(file: &str, span: Span, ty: &Type) -> Diagnostic {
|
|
Diagnostic::new(
|
|
file,
|
|
"UnsupportedResultPayloadType",
|
|
"results currently support only `(result i32 i32)`, `(result i64 i32)`, `(result u32 i32)`, `(result u64 i32)`, `(result f64 i32)`, `(result bool i32)`, and `(result string i32)`",
|
|
)
|
|
.with_span(span)
|
|
.expected("i32 ok with i32 err, i64 ok with i32 err, u32 ok with i32 err, u64 ok with i32 err, f64 ok with i32 err, bool ok with i32 err, or string ok with i32 err")
|
|
.found(ty.to_string())
|
|
}
|
|
|
|
fn check_array_init(
|
|
file: &str,
|
|
expr: &Expr,
|
|
elem_ty: &Type,
|
|
elem_ty_span: Span,
|
|
elements: &[Expr],
|
|
locals: &HashMap<String, Binding>,
|
|
functions: &HashMap<String, FnSig>,
|
|
structs: &HashMap<String, StructSig>,
|
|
enums: &HashMap<String, EnumSig>,
|
|
unsafe_context: bool,
|
|
) -> Result<TExpr, Diagnostic> {
|
|
if !array_element_type_supported_known(elem_ty, structs, enums) {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"UnsupportedArrayElementType",
|
|
supported_array_element_message(),
|
|
)
|
|
.with_span(elem_ty_span)
|
|
.expected(supported_array_element_expected())
|
|
.found(elem_ty.to_string()));
|
|
}
|
|
|
|
if elements.is_empty() {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"EmptyArrayUnsupported",
|
|
"first-pass arrays must contain at least one element",
|
|
)
|
|
.with_span(expr.span)
|
|
.hint("provide one or more supported direct scalar, string, enum, or struct values"));
|
|
}
|
|
|
|
let mut checked_elements = Vec::new();
|
|
for element in elements {
|
|
let checked = check_expr(
|
|
file,
|
|
element,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
)?;
|
|
if checked.ty != *elem_ty {
|
|
return Err(
|
|
Diagnostic::new(file, "TypeMismatch", "array element has the wrong type")
|
|
.with_span(checked.span)
|
|
.expected(elem_ty.to_string())
|
|
.found(checked.ty.to_string()),
|
|
);
|
|
}
|
|
checked_elements.push(checked);
|
|
}
|
|
|
|
Ok(TExpr {
|
|
ty: Type::Array(Box::new(elem_ty.clone()), checked_elements.len()),
|
|
span: expr.span,
|
|
kind: TExprKind::ArrayInit {
|
|
elements: checked_elements,
|
|
},
|
|
})
|
|
}
|
|
|
|
fn check_index(
|
|
file: &str,
|
|
expr: &Expr,
|
|
array: &Expr,
|
|
index: &Expr,
|
|
locals: &HashMap<String, Binding>,
|
|
functions: &HashMap<String, FnSig>,
|
|
structs: &HashMap<String, StructSig>,
|
|
enums: &HashMap<String, EnumSig>,
|
|
unsafe_context: bool,
|
|
) -> Result<TExpr, Diagnostic> {
|
|
let checked_array = check_expr(
|
|
file,
|
|
array,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
)?;
|
|
let Type::Array(_, len) = &checked_array.ty else {
|
|
return Err(
|
|
Diagnostic::new(file, "IndexOnNonArray", "`index` requires an array value")
|
|
.with_span(checked_array.span)
|
|
.expected("(array i32 N), (array i64 N), (array u32 N), (array u64 N), (array f64 N), (array bool N), (array string N), `(array KnownEnum N)`, or `(array KnownStruct N)`")
|
|
.found(checked_array.ty.to_string()),
|
|
);
|
|
};
|
|
let len = *len;
|
|
check_array_type(file, &checked_array.ty, checked_array.span, structs, enums)?;
|
|
|
|
let checked_index = check_expr(
|
|
file,
|
|
index,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
)?;
|
|
if checked_index.ty != Type::I32 {
|
|
return Err(
|
|
Diagnostic::new(file, "ArrayIndexNotI32", "`index` offset must be i32")
|
|
.with_span(checked_index.span)
|
|
.expected(Type::I32.to_string())
|
|
.found(checked_index.ty.to_string()),
|
|
);
|
|
}
|
|
|
|
if let TExprKind::Int(index_value) = &checked_index.kind {
|
|
if *index_value < 0 || *index_value as usize >= len {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"ArrayIndexOutOfBounds",
|
|
"array index is outside the fixed array bounds",
|
|
)
|
|
.with_span(checked_index.span)
|
|
.expected(array_index_range(len))
|
|
.found(index_value.to_string()));
|
|
}
|
|
}
|
|
|
|
let Type::Array(inner, _) = &checked_array.ty else {
|
|
unreachable!("checked array type disappeared");
|
|
};
|
|
|
|
Ok(TExpr {
|
|
ty: inner.as_ref().clone(),
|
|
span: expr.span,
|
|
kind: TExprKind::Index {
|
|
array: Box::new(checked_array),
|
|
index: Box::new(checked_index),
|
|
},
|
|
})
|
|
}
|
|
|
|
fn check_array_type_decl(
|
|
file: &str,
|
|
ty: &Type,
|
|
span: Span,
|
|
declared_struct_names: &HashMap<String, Span>,
|
|
enums: &HashMap<String, EnumSig>,
|
|
) -> Result<(), Diagnostic> {
|
|
let Type::Array(inner, len) = ty else {
|
|
return Ok(());
|
|
};
|
|
|
|
if *len == 0 {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"ZeroLengthArrayUnsupported",
|
|
"first-pass arrays must have positive length",
|
|
)
|
|
.with_span(span)
|
|
.hint("use one or more supported direct scalar, string, enum, or struct elements"));
|
|
}
|
|
|
|
if !array_element_type_supported_decl(inner, declared_struct_names, enums) {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"UnsupportedArrayElementType",
|
|
supported_array_element_message(),
|
|
)
|
|
.with_span(span)
|
|
.expected(supported_array_element_expected())
|
|
.found(inner.to_string()));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn check_array_type(
|
|
file: &str,
|
|
ty: &Type,
|
|
span: Span,
|
|
structs: &HashMap<String, StructSig>,
|
|
enums: &HashMap<String, EnumSig>,
|
|
) -> Result<(), Diagnostic> {
|
|
let Type::Array(inner, len) = ty else {
|
|
return Ok(());
|
|
};
|
|
|
|
if *len == 0 {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"ZeroLengthArrayUnsupported",
|
|
"first-pass arrays must have positive length",
|
|
)
|
|
.with_span(span)
|
|
.hint("use one or more supported direct scalar, string, enum, or struct elements"));
|
|
}
|
|
|
|
if !array_element_type_supported_known(inner, structs, enums) {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"UnsupportedArrayElementType",
|
|
supported_array_element_message(),
|
|
)
|
|
.with_span(span)
|
|
.expected(supported_array_element_expected())
|
|
.found(inner.to_string()));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn supported_array_element_message() -> &'static str {
|
|
"fixed arrays support direct scalar `i32`, `i64`, `u32`, `u64`, `f64`, `bool`, `string`, direct known enum, or current known non-recursive struct elements"
|
|
}
|
|
|
|
fn supported_array_element_expected() -> &'static str {
|
|
"i32, i64, u32, u64, f64, bool, string, direct known enum, or current known non-recursive struct type"
|
|
}
|
|
|
|
fn array_element_type_supported_decl(
|
|
ty: &Type,
|
|
declared_struct_names: &HashMap<String, Span>,
|
|
enums: &HashMap<String, EnumSig>,
|
|
) -> bool {
|
|
matches!(
|
|
ty,
|
|
Type::I32 | Type::I64 | Type::U32 | Type::U64 | Type::F64 | Type::Bool | Type::String
|
|
) || matches!(ty, Type::Named(name) if declared_struct_names.contains_key(name) || enums.contains_key(name))
|
|
}
|
|
|
|
fn array_element_type_supported_known(
|
|
ty: &Type,
|
|
structs: &HashMap<String, StructSig>,
|
|
enums: &HashMap<String, EnumSig>,
|
|
) -> bool {
|
|
matches!(
|
|
ty,
|
|
Type::I32 | Type::I64 | Type::U32 | Type::U64 | Type::F64 | Type::Bool | Type::String
|
|
) || is_known_struct_type(ty, structs)
|
|
|| is_known_enum_type(ty, enums)
|
|
}
|
|
|
|
fn check_vector_type(file: &str, ty: &Type, span: Span) -> Result<(), Diagnostic> {
|
|
let Type::Vec(inner) = ty else {
|
|
return Ok(());
|
|
};
|
|
|
|
if let Type::Named(name) = &**inner {
|
|
if is_generic_type_parameter_name(name) {
|
|
return Err(unsupported_generic_type_parameter(file, span, name));
|
|
}
|
|
}
|
|
|
|
if **inner != Type::I32
|
|
&& **inner != Type::I64
|
|
&& **inner != Type::F64
|
|
&& **inner != Type::Bool
|
|
&& **inner != Type::String
|
|
{
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"UnsupportedVectorElementType",
|
|
"vectors support only `i32`, `i64`, `f64`, `bool`, or `string` elements in the current concrete alpha slices",
|
|
)
|
|
.with_span(span)
|
|
.expected("i32, i64, f64, bool, or string")
|
|
.found(inner.to_string())
|
|
.hint("use exactly `(vec i32)`, `(vec i64)`, `(vec f64)`, `(vec bool)`, or `(vec string)` in the collections alpha slices"));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn unsupported_generic_type_parameter(file: &str, span: Span, name: &str) -> Diagnostic {
|
|
Diagnostic::new(
|
|
file,
|
|
"UnsupportedGenericTypeParameter",
|
|
format!(
|
|
"generic type parameter `{}` is reserved but not supported in beta.9",
|
|
name
|
|
),
|
|
)
|
|
.with_span(span)
|
|
.expected("concrete supported type")
|
|
.found(name.to_string())
|
|
.hint("use a concrete promoted type such as `i32`, `string`, or `(vec i32)`")
|
|
}
|
|
|
|
fn is_generic_type_parameter_name(name: &str) -> bool {
|
|
name.len() == 1 && name.bytes().all(|byte| byte.is_ascii_uppercase())
|
|
}
|
|
|
|
fn array_index_range(len: usize) -> String {
|
|
if len == 1 {
|
|
"0".to_string()
|
|
} else {
|
|
format!("0..{}", len - 1)
|
|
}
|
|
}
|
|
|
|
fn check_field_access(
|
|
file: &str,
|
|
expr: &Expr,
|
|
value: &Expr,
|
|
field: &str,
|
|
field_span: Span,
|
|
locals: &HashMap<String, Binding>,
|
|
functions: &HashMap<String, FnSig>,
|
|
structs: &HashMap<String, StructSig>,
|
|
enums: &HashMap<String, EnumSig>,
|
|
unsafe_context: bool,
|
|
) -> Result<TExpr, Diagnostic> {
|
|
let value = check_expr(
|
|
file,
|
|
value,
|
|
locals,
|
|
functions,
|
|
structs,
|
|
enums,
|
|
unsafe_context,
|
|
)?;
|
|
let Type::Named(struct_name) = &value.ty else {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"FieldAccessOnNonStruct",
|
|
"field access requires a struct value",
|
|
)
|
|
.with_span(value.span)
|
|
.expected("struct")
|
|
.found(value.ty.to_string()));
|
|
};
|
|
|
|
let sig = structs.get(struct_name).ok_or_else(|| {
|
|
Diagnostic::new(
|
|
file,
|
|
"UnknownStruct",
|
|
format!("unknown struct `{}`", struct_name),
|
|
)
|
|
.with_span(value.span)
|
|
})?;
|
|
|
|
let Some(index) = sig.fields_by_name.get(field) else {
|
|
return Err(Diagnostic::new(
|
|
file,
|
|
"UnknownStructField",
|
|
format!("struct `{}` has no field `{}`", struct_name, field),
|
|
)
|
|
.with_span(field_span));
|
|
};
|
|
|
|
Ok(TExpr {
|
|
ty: sig.fields[*index].ty.clone(),
|
|
span: expr.span,
|
|
kind: TExprKind::FieldAccess {
|
|
value: Box::new(value),
|
|
field: field.to_string(),
|
|
},
|
|
})
|
|
}
|
|
|
|
fn expect_type(file: &str, expr: &Expr, found: &Type, expected: &Type) -> Result<(), Diagnostic> {
|
|
if found == expected {
|
|
Ok(())
|
|
} else {
|
|
Err(Diagnostic::new(file, "TypeMismatch", "type mismatch")
|
|
.with_span(expr.span)
|
|
.expected(expected.to_string())
|
|
.found(found.to_string()))
|
|
}
|
|
}
|