use std::collections::HashMap;
use std::path::{Path, PathBuf};
use bock_air::{AIRNode, AirArg, EnumVariantPayload, NodeKind};
use bock_types::AIRModule;
use crate::error::CodegenError;
use crate::profile::TargetProfile;
#[derive(Debug, Clone)]
pub struct GeneratedCode {
pub files: Vec<OutputFile>,
}
#[derive(Debug, Clone)]
pub struct OutputFile {
pub path: PathBuf,
pub content: String,
pub source_map: Option<SourceMap>,
}
#[must_use]
pub fn derive_output_path(source_path: &Path, target: &TargetProfile) -> PathBuf {
use std::path::Component;
let mut comps: Vec<&std::ffi::OsStr> = source_path
.components()
.filter_map(|c| match c {
Component::Normal(s) => Some(s),
_ => None,
})
.collect();
if comps.first().and_then(|c| c.to_str()) == Some("src") {
comps.remove(0);
}
let stripped: PathBuf = comps.iter().collect();
stripped.with_extension(&target.conventions.file_extension)
}
#[derive(Debug, Clone, Default)]
pub struct SourceMap {
pub entries: Vec<SourceMapEntry>,
pub mappings: Vec<SourceMapping>,
pub generated_file: String,
pub sources: Vec<SourceInfo>,
}
#[derive(Debug, Clone)]
pub struct SourceMapEntry {
pub air_node_id: u32,
pub file_index: usize,
pub target_start: usize,
pub target_len: usize,
}
#[derive(Debug, Clone)]
pub struct SourceMapping {
pub gen_line: u32,
pub gen_col: u32,
pub src_line: u32,
pub src_col: u32,
pub src_offset: u32,
pub src_file_id: u32,
}
#[derive(Debug, Clone)]
pub struct SourceInfo {
pub path: String,
pub content: Option<String>,
}
impl SourceMap {
pub fn resolve_positions(&mut self, sources_content: &[&str]) {
for m in &mut self.mappings {
let Some(src) = sources_content.get(m.src_file_id as usize) else {
continue;
};
let (line, col) = byte_to_line_col(src, m.src_offset as usize);
m.src_line = line;
m.src_col = col;
}
}
#[must_use]
pub fn to_source_map_v3_json(&self) -> String {
let mut out = String::new();
out.push_str("{\"version\":3,\"file\":\"");
out.push_str(&escape_json(&self.generated_file));
out.push_str("\",\"sourceRoot\":\"\",\"sources\":[");
for (i, s) in self.sources.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push('"');
out.push_str(&escape_json(&s.path));
out.push('"');
}
out.push_str("],\"sourcesContent\":[");
for (i, s) in self.sources.iter().enumerate() {
if i > 0 {
out.push(',');
}
match &s.content {
Some(c) => {
out.push('"');
out.push_str(&escape_json(c));
out.push('"');
}
None => out.push_str("null"),
}
}
out.push_str("],\"names\":[],\"mappings\":\"");
out.push_str(&encode_vlq_mappings(&self.mappings));
out.push_str("\"}");
out
}
}
fn byte_to_line_col(src: &str, offset: usize) -> (u32, u32) {
let offset = offset.min(src.len());
let before = &src[..offset];
let line = before.bytes().filter(|b| *b == b'\n').count() as u32 + 1;
let line_start = before.rfind('\n').map_or(0, |i| i + 1);
let col = src[line_start..offset].chars().count() as u32 + 1;
(line, col)
}
fn escape_json(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\u{08}' => out.push_str("\\b"),
'\u{0C}' => out.push_str("\\f"),
c if (c as u32) < 0x20 => {
out.push_str(&format!("\\u{:04x}", c as u32));
}
c => out.push(c),
}
}
out
}
fn encode_vlq_mappings(mappings: &[SourceMapping]) -> String {
let mut resolved: Vec<&SourceMapping> = mappings.iter().filter(|m| m.src_line > 0).collect();
resolved.sort_by_key(|m| (m.gen_line, m.gen_col));
let mut out = String::new();
let mut prev_gen_line: u32 = 1;
let mut prev_gen_col: i64 = 0;
let mut prev_src_file: i64 = 0;
let mut prev_src_line: i64 = 0;
let mut prev_src_col: i64 = 0;
let mut first_on_line = true;
for m in resolved {
while prev_gen_line < m.gen_line {
out.push(';');
prev_gen_line += 1;
prev_gen_col = 0;
first_on_line = true;
}
if !first_on_line {
out.push(',');
}
let gen_col = (m.gen_col as i64) - 1;
let src_file = m.src_file_id as i64;
let src_line = (m.src_line as i64) - 1;
let src_col = (m.src_col as i64) - 1;
vlq_encode(&mut out, gen_col - prev_gen_col);
vlq_encode(&mut out, src_file - prev_src_file);
vlq_encode(&mut out, src_line - prev_src_line);
vlq_encode(&mut out, src_col - prev_src_col);
prev_gen_col = gen_col;
prev_src_file = src_file;
prev_src_line = src_line;
prev_src_col = src_col;
first_on_line = false;
}
out
}
fn vlq_encode(out: &mut String, value: i64) {
const BASE64: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut v: u64 = if value < 0 {
((-value as u64) << 1) | 1
} else {
(value as u64) << 1
};
loop {
let mut digit = (v & 0x1F) as u8;
v >>= 5;
if v != 0 {
digit |= 0x20;
}
out.push(BASE64[digit as usize] as char);
if v == 0 {
break;
}
}
}
pub trait CodeGenerator {
fn target(&self) -> &TargetProfile;
fn needs_ai_synthesis(&self, node: &bock_air::AIRNode) -> bool {
crate::ai_synthesis::needs_ai_synthesis(self.target(), node)
}
fn generate_module(&self, module: &AIRModule) -> Result<GeneratedCode, CodegenError>;
fn entry_invocation(&self, main_is_async: bool) -> Option<String> {
let _ = main_is_async;
None
}
fn generate_project(
&self,
modules: &[(&AIRModule, &Path)],
) -> Result<GeneratedCode, CodegenError>;
fn generate_tests(
&self,
modules: &[(&AIRModule, &Path)],
framework: &str,
) -> Result<TestArtifacts, CodegenError> {
let _ = (modules, framework);
Ok(TestArtifacts::default())
}
}
#[derive(Debug, Clone, Default)]
pub struct TestArtifacts {
pub files: Vec<OutputFile>,
pub entry_append: Option<String>,
}
#[must_use]
pub fn reachable_modules<'a>(
modules: &'a [(&'a AIRModule, &'a Path)],
) -> Vec<(&'a AIRModule, &'a Path)> {
let path_of = |m: &AIRModule| -> Option<String> {
if let NodeKind::Module { path: Some(p), .. } = &m.kind {
Some(
p.segments
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>()
.join("."),
)
} else {
None
}
};
let mut by_path: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
for (i, (m, _)) in modules.iter().enumerate() {
if let Some(p) = path_of(m) {
by_path.entry(p).or_insert(i);
}
}
let use_targets = |m: &AIRModule| -> Vec<String> {
let NodeKind::Module { imports, .. } = &m.kind else {
return vec![];
};
imports
.iter()
.filter_map(|imp| {
if let NodeKind::ImportDecl { path, .. } = &imp.kind {
Some(
path.segments
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>()
.join("."),
)
} else {
None
}
})
.collect()
};
let Some(entry_idx) = modules
.iter()
.position(|(m, _)| module_declares_main_fn(m))
.or_else(|| modules.len().checked_sub(1))
else {
return vec![];
};
let mut visited = vec![false; modules.len()];
let mut order: Vec<usize> = Vec::new();
enum Step {
Enter(usize),
Exit(usize),
}
let mut stack = vec![Step::Enter(entry_idx)];
while let Some(step) = stack.pop() {
match step {
Step::Enter(idx) => {
if visited[idx] {
continue;
}
visited[idx] = true;
stack.push(Step::Exit(idx));
let mut child_indices: Vec<usize> = use_targets(modules[idx].0)
.iter()
.filter_map(|target| by_path.get(target).copied())
.collect();
child_indices.sort_by(|&a, &b| {
path_of(modules[a].0)
.cmp(&path_of(modules[b].0))
.then(a.cmp(&b))
});
child_indices.dedup();
for child in child_indices.into_iter().rev() {
if !visited[child] {
stack.push(Step::Enter(child));
}
}
}
Step::Exit(idx) => order.push(idx),
}
}
order.into_iter().map(|i| modules[i]).collect()
}
#[must_use]
pub fn module_declares_main_fn(module: &AIRModule) -> bool {
let NodeKind::Module { items, .. } = &module.kind else {
return false;
};
items.iter().any(|item| {
matches!(
&item.kind,
NodeKind::FnDecl { name, .. } if name.name == "main"
)
})
}
#[must_use]
pub fn module_path_string(module: &AIRModule) -> Option<String> {
if let NodeKind::Module { path: Some(p), .. } = &module.kind {
Some(
p.segments
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>()
.join("."),
)
} else {
None
}
}
#[must_use]
pub fn module_main_fn_is_async(module: &AIRModule) -> bool {
let NodeKind::Module { items, .. } = &module.kind else {
return false;
};
items.iter().any(|item| {
matches!(
&item.kind,
NodeKind::FnDecl { name, is_async: true, .. } if name.name == "main"
)
})
}
#[must_use]
pub fn fn_is_test(node: &AIRNode) -> bool {
matches!(
&node.kind,
NodeKind::FnDecl { annotations, .. }
if annotations.iter().any(|a| a.name.name == "test")
)
}
#[must_use]
pub fn collect_test_fns<'a>(
modules: &'a [(&'a AIRModule, &'a Path)],
) -> Vec<(&'a AIRNode, String)> {
let mut tests = Vec::new();
for (module, _) in modules {
let module_path = module_path_string(module).unwrap_or_default();
if let NodeKind::Module { items, .. } = &module.kind {
for item in items {
if fn_is_test(item) {
tests.push((item, module_path.clone()));
}
}
}
}
tests
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TestAssertion {
Equal,
BeTrue,
BeFalse,
BeSome,
BeNone,
BeOk,
BeErr,
}
impl TestAssertion {
fn from_method(name: &str) -> Option<Self> {
match name {
"to_equal" => Some(Self::Equal),
"to_be_true" => Some(Self::BeTrue),
"to_be_false" => Some(Self::BeFalse),
"to_be_some" => Some(Self::BeSome),
"to_be_none" => Some(Self::BeNone),
"to_be_ok" => Some(Self::BeOk),
"to_be_err" => Some(Self::BeErr),
_ => None,
}
}
}
#[must_use]
pub fn classify_assertion(stmt: &AIRNode) -> Option<(TestAssertion, &AIRNode, Option<&AIRNode>)> {
let NodeKind::Call { callee, args, .. } = &stmt.kind else {
return None;
};
let NodeKind::FieldAccess { object, field } = &callee.kind else {
return None;
};
let assertion = TestAssertion::from_method(&field.name)?;
let NodeKind::Call {
callee: expect_callee,
args: expect_args,
..
} = &object.kind
else {
return None;
};
let NodeKind::Identifier { name } = &expect_callee.kind else {
return None;
};
if name.name != "expect" {
return None;
}
let actual = expect_args.first().map(|a| &a.value)?;
let expected = args.get(1).map(|a| &a.value);
Some((assertion, actual, expected))
}
pub const ESM_RUNTIME_PRELUDE_NAMES: &[&str] = &[
"Optional", "Some", "None", "Result", "Ok", "Err", "Less", "Equal", "Greater",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EsmDeclKind {
Function,
Const,
Record,
Class,
EnumType,
EnumVariant,
Trait,
Effect,
TypeAlias,
}
impl EsmDeclKind {
#[must_use]
pub fn is_js_value(self) -> bool {
matches!(
self,
EsmDeclKind::Function
| EsmDeclKind::Const
| EsmDeclKind::Record
| EsmDeclKind::Class
| EsmDeclKind::EnumVariant
| EsmDeclKind::Trait
| EsmDeclKind::Effect
)
}
#[must_use]
pub fn is_ts_type_only(self) -> bool {
matches!(
self,
EsmDeclKind::EnumType | EsmDeclKind::Trait | EsmDeclKind::TypeAlias
)
}
}
#[derive(Debug, Clone)]
pub struct EsmSymbol {
pub module_path: String,
pub kind: EsmDeclKind,
pub variant_bare_name: Option<String>,
}
impl EsmSymbol {
#[must_use]
pub fn is_fn(&self) -> bool {
matches!(self.kind, EsmDeclKind::Function)
}
}
#[must_use]
pub fn collect_public_symbols_for_esm(
modules: &[(&AIRModule, &Path)],
) -> HashMap<String, EsmSymbol> {
let mut map: HashMap<String, EsmSymbol> = HashMap::new();
for (module, _) in modules {
let Some(module_path) = module_path_string(module) else {
continue;
};
let NodeKind::Module { items, .. } = &module.kind else {
continue;
};
for item in items {
let mut record = |name: &str, kind: EsmDeclKind, variant_bare_name: Option<String>| {
if !ESM_RUNTIME_PRELUDE_NAMES.contains(&name) {
map.entry(name.to_string()).or_insert_with(|| EsmSymbol {
module_path: module_path.clone(),
kind,
variant_bare_name,
});
}
};
match &item.kind {
NodeKind::FnDecl {
visibility, name, ..
} => {
if matches!(visibility, bock_ast::Visibility::Public) {
record(&name.name, EsmDeclKind::Function, None);
}
}
NodeKind::RecordDecl {
visibility, name, ..
} => {
if matches!(visibility, bock_ast::Visibility::Public) {
record(&name.name, EsmDeclKind::Record, None);
}
}
NodeKind::ClassDecl {
visibility, name, ..
} => {
if matches!(visibility, bock_ast::Visibility::Public) {
record(&name.name, EsmDeclKind::Class, None);
}
}
NodeKind::TraitDecl {
visibility, name, ..
} => {
if matches!(visibility, bock_ast::Visibility::Public) {
record(&name.name, EsmDeclKind::Trait, None);
}
}
NodeKind::EffectDecl {
visibility, name, ..
} => {
if matches!(visibility, bock_ast::Visibility::Public) {
record(&name.name, EsmDeclKind::Effect, None);
}
}
NodeKind::TypeAlias {
visibility, name, ..
} => {
if matches!(visibility, bock_ast::Visibility::Public) {
record(&name.name, EsmDeclKind::TypeAlias, None);
}
}
NodeKind::ConstDecl {
visibility, name, ..
} => {
if matches!(visibility, bock_ast::Visibility::Public) {
record(&name.name, EsmDeclKind::Const, None);
}
}
NodeKind::EnumDecl {
visibility,
name,
variants,
..
} => {
if matches!(visibility, bock_ast::Visibility::Public) {
record(&name.name, EsmDeclKind::EnumType, None);
for v in variants {
if let NodeKind::EnumVariant { name: vname, .. } = &v.kind {
record(
&format!("{}_{}", name.name, vname.name),
EsmDeclKind::EnumVariant,
Some(vname.name.clone()),
);
}
}
}
}
_ => {}
}
}
}
map
}
#[derive(Debug, Clone)]
pub struct EsmExport {
pub name: String,
pub is_fn: bool,
}
#[must_use]
pub fn exportable_value_names(module: &AIRModule) -> Vec<EsmExport> {
let mut names: Vec<EsmExport> = Vec::new();
let mut push = |name: String, is_fn: bool| {
if !ESM_RUNTIME_PRELUDE_NAMES.contains(&name.as_str()) {
names.push(EsmExport { name, is_fn });
}
};
let NodeKind::Module { items, .. } = &module.kind else {
return names;
};
for item in items {
match &item.kind {
NodeKind::FnDecl {
visibility, name, ..
} => {
if matches!(visibility, bock_ast::Visibility::Public) {
push(name.name.clone(), true);
}
}
NodeKind::RecordDecl {
visibility, name, ..
}
| NodeKind::TraitDecl {
visibility, name, ..
}
| NodeKind::ClassDecl {
visibility, name, ..
}
| NodeKind::EffectDecl {
visibility, name, ..
}
| NodeKind::ConstDecl {
visibility, name, ..
} => {
if matches!(visibility, bock_ast::Visibility::Public) {
push(name.name.clone(), false);
}
}
NodeKind::EnumDecl {
visibility,
name,
variants,
..
} => {
if matches!(visibility, bock_ast::Visibility::Public) {
for v in variants {
if let NodeKind::EnumVariant { name: vname, .. } = &v.kind {
push(format!("{}_{}", name.name, vname.name), false);
}
}
}
}
_ => {}
}
}
names
}
#[must_use]
pub fn enum_variant_value_names(module: &AIRModule) -> Vec<String> {
let mut names: Vec<String> = Vec::new();
let NodeKind::Module { items, .. } = &module.kind else {
return names;
};
for item in items {
if let NodeKind::EnumDecl {
visibility,
name,
variants,
..
} = &item.kind
{
if matches!(visibility, bock_ast::Visibility::Public)
&& !ESM_RUNTIME_PRELUDE_NAMES.contains(&name.name.as_str())
{
for v in variants {
if let NodeKind::EnumVariant { name: vname, .. } = &v.kind {
names.push(format!("{}_{}", name.name, vname.name));
}
}
}
}
}
names
}
#[must_use]
pub fn locally_declared_names(module: &AIRModule) -> std::collections::HashSet<String> {
let mut names = std::collections::HashSet::new();
let NodeKind::Module { items, .. } = &module.kind else {
return names;
};
for item in items {
match &item.kind {
NodeKind::FnDecl { name, .. }
| NodeKind::RecordDecl { name, .. }
| NodeKind::TraitDecl { name, .. }
| NodeKind::ClassDecl { name, .. }
| NodeKind::EffectDecl { name, .. }
| NodeKind::TypeAlias { name, .. }
| NodeKind::ConstDecl { name, .. } => {
names.insert(name.name.clone());
}
NodeKind::EnumDecl { name, variants, .. } => {
names.insert(name.name.clone());
for v in variants {
if let NodeKind::EnumVariant { name: vname, .. } = &v.kind {
names.insert(format!("{}_{}", name.name, vname.name));
}
}
}
_ => {}
}
}
names
}
#[must_use]
pub fn explicitly_imported_names(module: &AIRModule) -> std::collections::HashSet<String> {
let mut names = std::collections::HashSet::new();
let NodeKind::Module { imports, .. } = &module.kind else {
return names;
};
for import in imports {
if let NodeKind::ImportDecl {
items: bock_ast::ImportItems::Named(named),
..
} = &import.kind
{
for n in named {
names.insert(n.name.name.clone());
if let Some(alias) = &n.alias {
names.insert(alias.name.clone());
}
}
}
}
names
}
#[derive(Debug, Clone)]
pub struct ImplicitEsmImport {
pub module_path: String,
pub name: String,
pub kind: EsmDeclKind,
}
impl ImplicitEsmImport {
#[must_use]
pub fn is_fn(&self) -> bool {
matches!(self.kind, EsmDeclKind::Function)
}
}
#[must_use]
pub fn implicit_esm_imports_for(
module: &AIRModule,
public_symbols: &HashMap<String, EsmSymbol>,
own_path: &str,
) -> Vec<ImplicitEsmImport> {
let local = locally_declared_names(module);
let explicit = explicitly_imported_names(module);
let rendered = format!("{module:?}");
let mut out: Vec<ImplicitEsmImport> = Vec::new();
for (name, sym) in public_symbols {
if sym.module_path == own_path || local.contains(name) || explicit.contains(name) {
continue;
}
let referenced = rendered.contains(&format!("\"{name}\""))
|| sym
.variant_bare_name
.as_ref()
.is_some_and(|bare| rendered.contains(&format!("\"{bare}\"")));
if referenced {
out.push(ImplicitEsmImport {
module_path: sym.module_path.clone(),
name: name.clone(),
kind: sym.kind,
});
}
}
out
}
#[must_use]
pub fn collect_record_names(modules: &[(&AIRModule, &Path)]) -> std::collections::HashSet<String> {
let mut names = std::collections::HashSet::new();
for (module, _) in modules {
let NodeKind::Module { items, .. } = &module.kind else {
continue;
};
for item in items {
if let NodeKind::RecordDecl { name, .. } = &item.kind {
names.insert(name.name.clone());
}
}
}
names
}
#[must_use]
pub fn collect_class_fields(
modules: &[(&AIRModule, &Path)],
) -> std::collections::HashMap<String, Vec<String>> {
let mut classes = std::collections::HashMap::new();
for (module, _) in modules {
let NodeKind::Module { items, .. } = &module.kind else {
continue;
};
for item in items {
if let NodeKind::ClassDecl { name, fields, .. } = &item.kind {
let field_order = fields.iter().map(|f| f.name.name.clone()).collect();
classes.insert(name.name.clone(), field_order);
}
}
}
classes
}
#[must_use]
pub fn collect_const_names(modules: &[(&AIRModule, &Path)]) -> std::collections::HashSet<String> {
let mut names = std::collections::HashSet::new();
for (module, _) in modules {
let NodeKind::Module { items, .. } = &module.kind else {
continue;
};
for item in items {
if let NodeKind::ConstDecl { name, .. } = &item.kind {
names.insert(name.name.clone());
}
}
}
names
}
#[must_use]
pub fn esm_relative_specifier(from_path: &str, to_path: &str, ext: &str) -> String {
let from_dirs: Vec<&str> = if from_path.is_empty() {
Vec::new()
} else {
let segs: Vec<&str> = from_path.split('.').collect();
segs[..segs.len().saturating_sub(1)].to_vec()
};
let to_segs: Vec<&str> = to_path.split('.').filter(|s| !s.is_empty()).collect();
let mut common = 0usize;
while common < from_dirs.len()
&& common + 1 < to_segs.len()
&& from_dirs[common] == to_segs[common]
{
common += 1;
}
let ups = from_dirs.len() - common;
let mut spec = String::new();
if ups == 0 {
spec.push_str("./");
} else {
for _ in 0..ups {
spec.push_str("../");
}
}
let down: Vec<&str> = to_segs[common..].to_vec();
spec.push_str(&down.join("/"));
spec.push('.');
spec.push_str(ext);
spec
}
pub trait JsTsExprEmitter {
fn expr_to_string(&mut self, node: &AIRNode) -> Result<String, CodegenError>;
}
fn js_camel_case(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut upper_next = false;
for (i, c) in s.chars().enumerate() {
if c == '_' {
upper_next = true;
} else if i == 0 {
out.push(c.to_ascii_lowercase());
} else if upper_next {
out.push(c.to_ascii_uppercase());
upper_next = false;
} else {
out.push(c);
}
}
out
}
pub fn js_ts_generate_tests<'a, F, M>(
modules: &'a [(&'a AIRModule, &'a Path)],
framework: &str,
file_ext: &str,
import_ext: &str,
output_path: F,
make_emitter: M,
) -> Result<TestArtifacts, CodegenError>
where
F: Fn(&'a AIRModule, &'a Path, bool) -> PathBuf,
M: for<'b> FnOnce(&'b [(&'b AIRModule, &'b Path)]) -> Box<dyn JsTsExprEmitter>,
{
let reachable = reachable_modules(modules);
let tests = collect_test_fns(&reachable);
if tests.is_empty() {
return Ok(TestArtifacts::default());
}
let entry_idx = reachable
.iter()
.position(|(m, _)| module_declares_main_fn(m))
.unwrap_or(reachable.len().saturating_sub(1));
let mut import_lines: Vec<String> = Vec::new();
for (i, (module, source_path)) in reachable.iter().enumerate() {
let mut import_names: Vec<String> = exportable_value_names(module)
.into_iter()
.filter(|e| e.is_fn)
.map(|e| js_camel_case(&e.name))
.collect();
import_names.extend(enum_variant_value_names(module));
if import_names.is_empty() {
continue;
}
let spec = if i == entry_idx {
let stem = output_path(module, source_path, true)
.with_extension(import_ext)
.display()
.to_string();
format!("./{stem}")
} else {
let to_path = module_path_string(module).unwrap_or_default();
esm_relative_specifier("", &to_path, import_ext)
};
let spec = spec.replace('\\', "/");
import_lines.push(format!(
"import {{ {} }} from \"{spec}\";",
import_names.join(", ")
));
}
import_lines.sort_unstable();
import_lines.dedup();
let is_jest = framework == "jest";
let mut out = String::new();
if is_jest {
out.push_str("// Jest provides describe/it/expect as globals.\n");
} else {
out.push_str("import { describe, it, expect } from \"vitest\";\n");
}
for line in &import_lines {
out.push_str(line);
out.push('\n');
}
out.push('\n');
out.push_str("describe(\"bock tests\", () => {\n");
let mut emitter = make_emitter(&reachable);
for (test_fn, _module_path) in &tests {
let NodeKind::FnDecl { name, body, .. } = &test_fn.kind else {
continue;
};
out.push_str(&format!(" it(\"{}\", () => {{\n", name.name));
emit_js_test_body(body, emitter.as_mut(), &mut out)?;
out.push_str(" });\n");
}
out.push_str("});\n");
Ok(TestArtifacts {
files: vec![OutputFile {
path: PathBuf::from(format!("bock.test.{file_ext}")),
content: out,
source_map: None,
}],
entry_append: None,
})
}
fn js_actual_needs_member_parens(actual: &AIRNode) -> bool {
!matches!(
&actual.kind,
NodeKind::Identifier { .. }
| NodeKind::Literal { .. }
| NodeKind::Call { .. }
| NodeKind::MethodCall { .. }
| NodeKind::FieldAccess { .. }
| NodeKind::Index { .. }
)
}
fn emit_js_test_body(
body: &AIRNode,
emitter: &mut dyn JsTsExprEmitter,
out: &mut String,
) -> Result<(), CodegenError> {
let stmts: Vec<&AIRNode> = match &body.kind {
NodeKind::Block { stmts, tail } => stmts.iter().chain(tail.as_deref()).collect(),
_ => vec![body],
};
for stmt in stmts {
if let Some((assertion, actual, expected)) = classify_assertion(stmt) {
let a = emitter.expr_to_string(actual)?;
let tagged = |a: &str| -> String {
if js_actual_needs_member_parens(actual) {
format!("({a})._tag")
} else {
format!("{a}._tag")
}
};
let line = match assertion {
TestAssertion::Equal => {
let e = match expected {
Some(e) => emitter.expr_to_string(e)?,
None => "undefined".to_string(),
};
format!("expect({a}).toEqual({e});")
}
TestAssertion::BeTrue => format!("expect({a}).toBe(true);"),
TestAssertion::BeFalse => format!("expect({a}).toBe(false);"),
TestAssertion::BeSome => format!("expect({}).toBe(\"Some\");", tagged(&a)),
TestAssertion::BeNone => format!("expect({}).toBe(\"None\");", tagged(&a)),
TestAssertion::BeOk => format!("expect({}).toBe(\"Ok\");", tagged(&a)),
TestAssertion::BeErr => format!("expect({}).toBe(\"Err\");", tagged(&a)),
};
out.push_str(&format!(" {line}\n"));
} else if let NodeKind::LetBinding { pattern, value, .. } = &stmt.kind {
let name = match &pattern.kind {
NodeKind::BindPat { name, .. } => js_camel_case(&name.name),
_ => continue,
};
let v = emitter.expr_to_string(value)?;
out.push_str(&format!(" const {name} = {v};\n"));
} else {
let s = emitter.expr_to_string(stmt)?;
out.push_str(&format!(" {s};\n"));
}
}
Ok(())
}
#[must_use]
pub fn collect_public_symbol_modules(modules: &[(&AIRModule, &Path)]) -> HashMap<String, String> {
let mut map: HashMap<String, String> = HashMap::new();
for (module, _) in modules {
let Some(module_path) = module_path_string(module) else {
continue;
};
let NodeKind::Module { items, .. } = &module.kind else {
continue;
};
for item in items {
let mut record = |name: &str| {
map.entry(name.to_string())
.or_insert_with(|| module_path.clone());
};
match &item.kind {
NodeKind::FnDecl {
visibility, name, ..
}
| NodeKind::RecordDecl {
visibility, name, ..
}
| NodeKind::TraitDecl {
visibility, name, ..
}
| NodeKind::ClassDecl {
visibility, name, ..
}
| NodeKind::EffectDecl {
visibility, name, ..
}
| NodeKind::TypeAlias {
visibility, name, ..
}
| NodeKind::ConstDecl {
visibility, name, ..
}
| NodeKind::EnumDecl {
visibility, name, ..
} => {
if matches!(visibility, bock_ast::Visibility::Public) {
record(&name.name);
}
}
_ => {}
}
}
}
map
}
#[must_use]
pub fn implicit_imports_for(
module: &AIRModule,
public_symbols: &HashMap<String, String>,
own_path: &str,
) -> Vec<(String, String)> {
let local = locally_declared_names(module);
let explicit = explicitly_imported_names(module);
let rendered = format!("{module:?}");
let mut out: Vec<(String, String)> = Vec::new();
for (name, declaring_module) in public_symbols {
if declaring_module == own_path || local.contains(name) || explicit.contains(name) {
continue;
}
if rendered.contains(&format!("\"{name}\"")) {
out.push((declaring_module.clone(), name.clone()));
}
}
out
}
#[must_use]
pub fn module_tree_relpath(
module: &AIRModule,
source_path: &Path,
target: &TargetProfile,
) -> PathBuf {
match module_path_string(module) {
Some(path) if !path.is_empty() => {
let rel: PathBuf = path.split('.').collect();
rel.with_extension(&target.conventions.file_extension)
}
_ => derive_output_path(source_path, target),
}
}
#[must_use]
pub fn node_is_statement(node: &AIRNode) -> bool {
if let NodeKind::If {
then_block,
else_block,
..
} = &node.kind
{
return match else_block {
None => true,
Some(else_b) => arm_body_is_statement(then_block) && arm_body_is_statement(else_b),
};
}
matches!(
node.kind,
NodeKind::Break { .. }
| NodeKind::Continue
| NodeKind::Return { .. }
| NodeKind::Assign { .. }
)
}
#[must_use]
pub fn arm_body_is_statement(body: &AIRNode) -> bool {
if node_is_statement(body) {
return true;
}
if let NodeKind::Block { tail, .. } = &body.kind {
return match tail {
Some(t) => node_is_statement(t),
None => true,
};
}
false
}
#[must_use]
pub fn match_has_statement_arm(arms: &[AIRNode]) -> bool {
arms.iter().any(
|arm| matches!(&arm.kind, NodeKind::MatchArm { body, .. } if arm_body_is_statement(body)),
)
}
#[must_use]
pub fn match_needs_ifchain(arms: &[AIRNode]) -> bool {
arms.iter().any(|arm| {
let NodeKind::MatchArm { pattern, guard, .. } = &arm.kind else {
return false;
};
guard.is_some() || pattern_needs_ifchain(pattern)
})
}
fn pattern_needs_ifchain(pat: &AIRNode) -> bool {
match &pat.kind {
NodeKind::OrPat { .. }
| NodeKind::TuplePat { .. }
| NodeKind::ListPat { .. }
| NodeKind::RangePat { .. } => true,
NodeKind::ConstructorPat { fields, .. } => fields.iter().any(field_is_structured),
NodeKind::RecordPat { fields, .. } => fields
.iter()
.filter_map(|f| f.pattern.as_deref())
.any(field_is_structured),
_ => false,
}
}
fn field_is_structured(pat: &AIRNode) -> bool {
!matches!(&pat.kind, NodeKind::WildcardPat | NodeKind::BindPat { .. })
}
pub const DECL_ONLY_META: &str = "bock_decl_only";
const SPLICE_BLOCK_META: &str = "bock_splice_block";
#[must_use]
pub fn value_cf_diverges(node: &AIRNode) -> bool {
match &node.kind {
NodeKind::Loop { body } => loop_has_value_break(body),
NodeKind::If {
then_block,
else_block,
let_pattern: None,
..
} => {
let branches = [Some(then_block.as_ref()), else_block.as_deref()];
let any_diverges = branches
.iter()
.flatten()
.any(|b| branch_diverges_or_nested(b));
let any_value = branches.iter().flatten().any(|b| branch_yields_value(b));
any_diverges && any_value
}
NodeKind::Match { arms, .. } => {
let bodies: Vec<&AIRNode> = arms
.iter()
.filter_map(|a| match &a.kind {
NodeKind::MatchArm { body, .. } => Some(body.as_ref()),
_ => None,
})
.collect();
let any_diverges = bodies.iter().any(|b| branch_diverges_or_nested(b));
let any_value = bodies.iter().any(|b| branch_yields_value(b));
any_diverges && any_value
}
NodeKind::Block { tail, .. } => tail.as_deref().is_some_and(value_cf_diverges),
_ => false,
}
}
fn branch_diverges_or_nested(node: &AIRNode) -> bool {
branch_tail_diverges(node) || value_cf_diverges(node)
}
fn branch_tail_diverges(node: &AIRNode) -> bool {
match &node.kind {
NodeKind::Return { .. } | NodeKind::Break { .. } | NodeKind::Continue => true,
NodeKind::Unreachable => true,
NodeKind::Call { .. } => call_is_diverging_intrinsic(node),
NodeKind::Block { stmts, tail } => match tail {
Some(t) => branch_tail_diverges(t),
None => stmts.last().is_some_and(branch_tail_diverges),
},
NodeKind::If {
then_block,
else_block: Some(else_b),
..
} => branch_tail_diverges(then_block) && branch_tail_diverges(else_b),
NodeKind::Match { arms, .. } => {
let bodies: Vec<&AIRNode> = arms
.iter()
.filter_map(|a| match &a.kind {
NodeKind::MatchArm { body, .. } => Some(body.as_ref()),
_ => None,
})
.collect();
!bodies.is_empty() && bodies.iter().all(|b| branch_tail_diverges(b))
}
_ => false,
}
}
fn branch_yields_value(node: &AIRNode) -> bool {
if value_cf_diverges(node) {
return true;
}
!branch_tail_diverges(node)
}
fn loop_has_value_break(body: &AIRNode) -> bool {
match &body.kind {
NodeKind::Break { value } => value.is_some(),
NodeKind::Loop { .. }
| NodeKind::While { .. }
| NodeKind::For { .. }
| NodeKind::FnDecl { .. }
| NodeKind::Lambda { .. } => false,
NodeKind::Block { stmts, tail } => {
stmts.iter().any(loop_has_value_break)
|| tail.as_deref().is_some_and(loop_has_value_break)
}
NodeKind::If {
then_block,
else_block,
..
} => {
loop_has_value_break(then_block)
|| else_block.as_deref().is_some_and(loop_has_value_break)
}
NodeKind::Match { arms, .. } => arms.iter().any(
|a| matches!(&a.kind, NodeKind::MatchArm { body, .. } if loop_has_value_break(body)),
),
NodeKind::Guard { else_block, .. } => loop_has_value_break(else_block),
_ => false,
}
}
fn call_is_diverging_intrinsic(node: &AIRNode) -> bool {
let NodeKind::Call { callee, .. } = &node.kind else {
return false;
};
matches!(
&callee.kind,
NodeKind::Identifier { name } if name.name == "todo" || name.name == "unreachable"
)
}
#[must_use]
pub fn lower_blanket_into(module: AIRNode) -> AIRNode {
let targets = collect_from_targets(&module);
let [target] = targets.as_slice() else {
return module;
};
let mut rewriter = BlanketIntoRewriter {
next_id: max_node_id(&module) + 1,
target: target.clone(),
};
rewriter.rewrite(module)
}
fn collect_from_targets(module: &AIRNode) -> Vec<String> {
let NodeKind::Module { items, .. } = &module.kind else {
return Vec::new();
};
let mut targets: Vec<String> = items
.iter()
.filter_map(|item| {
let NodeKind::ImplBlock {
trait_path: Some(tp),
target,
..
} = &item.kind
else {
return None;
};
if tp.segments.last().map(|s| s.name.as_str()) != Some("From") {
return None;
}
type_node_base_name(target)
})
.collect();
targets.sort();
targets.dedup();
targets
}
fn type_node_base_name(ty: &AIRNode) -> Option<String> {
if let NodeKind::TypeNamed { path, .. } = &ty.kind {
path.segments.last().map(|s| s.name.clone())
} else {
None
}
}
struct BlanketIntoRewriter {
next_id: bock_air::NodeId,
target: String,
}
impl BlanketIntoRewriter {
fn fresh_id(&mut self) -> bock_air::NodeId {
let id = self.next_id;
self.next_id += 1;
id
}
fn rewrite(&mut self, mut node: AIRNode) -> AIRNode {
if let NodeKind::Call { callee, args, .. } = &node.kind {
if let Some((recv, method, rest)) = desugared_self_call(callee, args) {
if method.name == "into" && rest.is_empty() {
let span = node.span;
let recv = recv.clone();
let target_id =
AIRNode::new(self.fresh_id(), span, ident_node(&self.target, span));
let field = AIRNode::new(
self.fresh_id(),
span,
NodeKind::FieldAccess {
object: Box::new(target_id),
field: bock_ast::Ident {
name: "from".to_string(),
span,
},
},
);
let mut call = AIRNode::new(
self.fresh_id(),
span,
NodeKind::Call {
callee: Box::new(field),
args: vec![AirArg {
label: None,
value: self.rewrite(recv),
}],
type_args: vec![],
},
);
call.metadata.insert(
bock_air::lower::ASSOC_CALL_META_KEY.to_string(),
bock_air::Value::Bool(true),
);
return call;
}
}
}
node.kind = self.rewrite_kind(node.kind);
node
}
fn rewrite_box(&mut self, node: Box<AIRNode>) -> Box<AIRNode> {
Box::new(self.rewrite(*node))
}
fn rewrite_vec(&mut self, nodes: Vec<AIRNode>) -> Vec<AIRNode> {
nodes.into_iter().map(|n| self.rewrite(n)).collect()
}
fn rewrite_args(&mut self, args: Vec<AirArg>) -> Vec<AirArg> {
args.into_iter()
.map(|a| AirArg {
label: a.label,
value: self.rewrite(a.value),
})
.collect()
}
fn rewrite_kind(&mut self, kind: NodeKind) -> NodeKind {
match kind {
NodeKind::Module {
path,
annotations,
imports,
items,
} => NodeKind::Module {
path,
annotations,
imports,
items: self.rewrite_vec(items),
},
NodeKind::FnDecl {
annotations,
visibility,
is_async,
name,
generic_params,
params,
return_type,
effect_clause,
where_clause,
body,
} => NodeKind::FnDecl {
annotations,
visibility,
is_async,
name,
generic_params,
params,
return_type,
effect_clause,
where_clause,
body: self.rewrite_box(body),
},
NodeKind::ImplBlock {
annotations,
generic_params,
trait_path,
trait_args,
target,
where_clause,
methods,
} => NodeKind::ImplBlock {
annotations,
generic_params,
trait_path,
trait_args,
target,
where_clause,
methods: self.rewrite_vec(methods),
},
NodeKind::ClassDecl {
annotations,
visibility,
name,
generic_params,
base,
traits,
fields,
methods,
} => NodeKind::ClassDecl {
annotations,
visibility,
name,
generic_params,
base,
traits,
fields,
methods: self.rewrite_vec(methods),
},
NodeKind::Block { stmts, tail } => NodeKind::Block {
stmts: self.rewrite_vec(stmts),
tail: tail.map(|t| self.rewrite_box(t)),
},
NodeKind::LetBinding {
pattern,
ty,
value,
is_mut,
} => NodeKind::LetBinding {
pattern,
ty,
value: self.rewrite_box(value),
is_mut,
},
NodeKind::Assign { target, op, value } => NodeKind::Assign {
target: self.rewrite_box(target),
op,
value: self.rewrite_box(value),
},
NodeKind::Call {
callee,
args,
type_args,
} => NodeKind::Call {
callee: self.rewrite_box(callee),
args: self.rewrite_args(args),
type_args,
},
NodeKind::MethodCall {
receiver,
method,
args,
type_args,
} => NodeKind::MethodCall {
receiver: self.rewrite_box(receiver),
method,
args: self.rewrite_args(args),
type_args,
},
NodeKind::FieldAccess { object, field } => NodeKind::FieldAccess {
object: self.rewrite_box(object),
field,
},
NodeKind::Index { object, index } => NodeKind::Index {
object: self.rewrite_box(object),
index: self.rewrite_box(index),
},
NodeKind::BinaryOp { op, left, right } => NodeKind::BinaryOp {
op,
left: self.rewrite_box(left),
right: self.rewrite_box(right),
},
NodeKind::UnaryOp { op, operand } => NodeKind::UnaryOp {
op,
operand: self.rewrite_box(operand),
},
NodeKind::Propagate { expr } => NodeKind::Propagate {
expr: self.rewrite_box(expr),
},
NodeKind::Await { expr } => NodeKind::Await {
expr: self.rewrite_box(expr),
},
NodeKind::Move { expr } => NodeKind::Move {
expr: self.rewrite_box(expr),
},
NodeKind::Borrow { expr } => NodeKind::Borrow {
expr: self.rewrite_box(expr),
},
NodeKind::MutableBorrow { expr } => NodeKind::MutableBorrow {
expr: self.rewrite_box(expr),
},
NodeKind::Return { value } => NodeKind::Return {
value: value.map(|v| self.rewrite_box(v)),
},
NodeKind::Lambda { params, body } => NodeKind::Lambda {
params,
body: self.rewrite_box(body),
},
NodeKind::If {
let_pattern,
condition,
then_block,
else_block,
} => NodeKind::If {
let_pattern,
condition: self.rewrite_box(condition),
then_block: self.rewrite_box(then_block),
else_block: else_block.map(|e| self.rewrite_box(e)),
},
NodeKind::Match { scrutinee, arms } => NodeKind::Match {
scrutinee: self.rewrite_box(scrutinee),
arms: self.rewrite_vec(arms),
},
NodeKind::MatchArm {
pattern,
guard,
body,
} => NodeKind::MatchArm {
pattern,
guard: guard.map(|g| self.rewrite_box(g)),
body: self.rewrite_box(body),
},
NodeKind::Guard {
let_pattern,
condition,
else_block,
} => NodeKind::Guard {
let_pattern,
condition: self.rewrite_box(condition),
else_block: self.rewrite_box(else_block),
},
NodeKind::While { condition, body } => NodeKind::While {
condition: self.rewrite_box(condition),
body: self.rewrite_box(body),
},
NodeKind::Loop { body } => NodeKind::Loop {
body: self.rewrite_box(body),
},
NodeKind::For {
pattern,
iterable,
body,
} => NodeKind::For {
pattern,
iterable: self.rewrite_box(iterable),
body: self.rewrite_box(body),
},
NodeKind::ListLiteral { elems } => NodeKind::ListLiteral {
elems: self.rewrite_vec(elems),
},
NodeKind::SetLiteral { elems } => NodeKind::SetLiteral {
elems: self.rewrite_vec(elems),
},
NodeKind::TupleLiteral { elems } => NodeKind::TupleLiteral {
elems: self.rewrite_vec(elems),
},
NodeKind::Pipe { left, right } => NodeKind::Pipe {
left: self.rewrite_box(left),
right: self.rewrite_box(right),
},
NodeKind::Compose { left, right } => NodeKind::Compose {
left: self.rewrite_box(left),
right: self.rewrite_box(right),
},
NodeKind::Range { lo, hi, inclusive } => NodeKind::Range {
lo: self.rewrite_box(lo),
hi: self.rewrite_box(hi),
inclusive,
},
NodeKind::RecordConstruct {
path,
fields,
spread,
} => NodeKind::RecordConstruct {
path,
fields: fields
.into_iter()
.map(|f| bock_air::AirRecordField {
name: f.name,
value: f.value.map(|v| self.rewrite_box(v)),
})
.collect(),
spread: spread.map(|s| self.rewrite_box(s)),
},
NodeKind::MapLiteral { entries } => NodeKind::MapLiteral {
entries: entries
.into_iter()
.map(|e| bock_air::AirMapEntry {
key: self.rewrite(e.key),
value: self.rewrite(e.value),
})
.collect(),
},
NodeKind::Interpolation { parts } => NodeKind::Interpolation {
parts: parts
.into_iter()
.map(|p| match p {
bock_air::AirInterpolationPart::Expr(e) => {
bock_air::AirInterpolationPart::Expr(self.rewrite_box(e))
}
lit @ bock_air::AirInterpolationPart::Literal(_) => lit,
})
.collect(),
},
NodeKind::ResultConstruct { variant, value } => NodeKind::ResultConstruct {
variant,
value: value.map(|v| self.rewrite_box(v)),
},
NodeKind::Break { value } => NodeKind::Break {
value: value.map(|v| self.rewrite_box(v)),
},
NodeKind::EffectOp {
effect,
operation,
args,
} => NodeKind::EffectOp {
effect,
operation,
args: self.rewrite_args(args),
},
NodeKind::HandlingBlock { handlers, body } => NodeKind::HandlingBlock {
handlers: handlers
.into_iter()
.map(|h| bock_air::AirHandlerPair {
effect: h.effect,
handler: self.rewrite_box(h.handler),
})
.collect(),
body: self.rewrite_box(body),
},
other => other,
}
}
}
fn ident_node(name: &str, span: bock_errors::Span) -> NodeKind {
NodeKind::Identifier {
name: bock_ast::Ident {
name: name.to_string(),
span,
},
}
}
pub fn hoist_value_cf(module: AIRNode) -> AIRNode {
let mut hoister = ValueCfHoister {
next_id: max_node_id(&module) + 1,
counter: 0,
prelude: Vec::new(),
};
hoister.rewrite(module)
}
fn max_node_id(node: &AIRNode) -> bock_air::NodeId {
struct MaxId(bock_air::NodeId);
impl bock_air::visitor::Visitor for MaxId {
fn visit_node(&mut self, node: &AIRNode) {
self.0 = self.0.max(node.id);
bock_air::visitor::walk_node(self, node);
}
}
let mut m = MaxId(0);
use bock_air::visitor::Visitor;
m.visit_node(node);
m.0
}
struct ValueCfHoister {
next_id: bock_air::NodeId,
counter: u32,
prelude: Vec<AIRNode>,
}
impl ValueCfHoister {
fn fresh_id(&mut self) -> bock_air::NodeId {
let id = self.next_id;
self.next_id += 1;
id
}
fn fresh_temp_name(&mut self) -> String {
let n = self.counter;
self.counter += 1;
format!("__bock_cf_{n}")
}
fn node(&mut self, span: bock_errors::Span, kind: NodeKind) -> AIRNode {
AIRNode::new(self.fresh_id(), span, kind)
}
fn rewrite(&mut self, mut node: AIRNode) -> AIRNode {
node.kind = self.rewrite_kind(node.kind, node.span);
node
}
fn rewrite_box(&mut self, node: Box<AIRNode>) -> Box<AIRNode> {
Box::new(self.rewrite(*node))
}
fn rewrite_value(&mut self, node: AIRNode) -> AIRNode {
if value_cf_diverges(&node) {
self.hoist(node)
} else {
self.rewrite(node)
}
}
fn rewrite_value_box(&mut self, node: Box<AIRNode>) -> Box<AIRNode> {
Box::new(self.rewrite_value(*node))
}
fn hoist(&mut self, cf: AIRNode) -> AIRNode {
let span = cf.span;
let temp = self.fresh_temp_name();
let decl_pat = self.node(
span,
NodeKind::BindPat {
name: bock_ast::Ident {
name: temp.clone(),
span,
},
is_mut: true,
},
);
let placeholder = self.node(span, NodeKind::Unreachable);
let mut decl = self.node(
span,
NodeKind::LetBinding {
is_mut: true,
pattern: Box::new(decl_pat),
ty: None,
value: Box::new(placeholder),
},
);
decl.metadata.insert(
DECL_ONLY_META.to_string(),
bock_air::stubs::Value::Bool(true),
);
let stmt_cf = self.rewrite_to_assign(cf, &temp);
self.prelude.push(decl);
self.prelude.push(stmt_cf);
self.node(
span,
NodeKind::Identifier {
name: bock_ast::Ident {
name: temp.clone(),
span,
},
},
)
}
fn rewrite_stmts(&mut self, stmts: Vec<AIRNode>) -> Vec<AIRNode> {
let mut out = Vec::with_capacity(stmts.len());
for stmt in stmts {
let saved = std::mem::take(&mut self.prelude);
let rewritten = self.rewrite(stmt);
let prelude = std::mem::replace(&mut self.prelude, saved);
out.extend(prelude);
out.push(rewritten);
}
out
}
fn rewrite_body(&mut self, body: Box<AIRNode>) -> Box<AIRNode> {
let body = *body;
let NodeKind::Block { stmts, tail } = body.kind else {
return Box::new(self.rewrite_value(body));
};
let mut out_stmts = self.rewrite_stmts(stmts);
let new_tail = match tail {
Some(t) => {
let saved = std::mem::take(&mut self.prelude);
let rewritten = self.rewrite_value(*t);
let prelude = std::mem::replace(&mut self.prelude, saved);
out_stmts.extend(prelude);
Some(Box::new(rewritten))
}
None => None,
};
Box::new(AIRNode::new(
body.id,
body.span,
NodeKind::Block {
stmts: out_stmts,
tail: new_tail,
},
))
}
fn rewrite_to_assign(&mut self, node: AIRNode, temp: &str) -> AIRNode {
let span = node.span;
match node.kind {
NodeKind::Block { stmts, tail } => {
let mut stmts = self.rewrite_stmts(stmts);
if let Some(t) = tail {
let saved = std::mem::take(&mut self.prelude);
let assigned = self.rewrite_to_assign(*t, temp);
let prelude = std::mem::replace(&mut self.prelude, saved);
stmts.extend(prelude);
stmts.push(assigned);
}
AIRNode::new(node.id, span, NodeKind::Block { stmts, tail: None })
}
NodeKind::If {
let_pattern,
condition,
then_block,
else_block,
} => {
let condition = self.rewrite_box(condition);
let then_block = Box::new(self.rewrite_to_assign(*then_block, temp));
let else_block = else_block.map(|e| Box::new(self.rewrite_to_assign(*e, temp)));
AIRNode::new(
node.id,
span,
NodeKind::If {
let_pattern,
condition,
then_block,
else_block,
},
)
}
NodeKind::Match { scrutinee, arms } => {
let scrutinee = self.rewrite_box(scrutinee);
let arms = arms
.into_iter()
.map(|arm| match arm.kind {
NodeKind::MatchArm {
pattern,
guard,
body,
} => {
let body = Box::new(self.rewrite_to_assign(*body, temp));
AIRNode::new(
arm.id,
arm.span,
NodeKind::MatchArm {
pattern,
guard,
body,
},
)
}
other => AIRNode::new(arm.id, arm.span, other),
})
.collect();
AIRNode::new(node.id, span, NodeKind::Match { scrutinee, arms })
}
NodeKind::Loop { body } => {
let body = Box::new(self.rewrite_breaks_to_assign(*body, temp));
AIRNode::new(node.id, span, NodeKind::Loop { body })
}
_ if branch_tail_diverges(&AIRNode::new(node.id, span, node.kind.clone())) => {
AIRNode::new(node.id, span, self.rewrite_kind(node.kind, span))
}
_ => {
let saved = std::mem::take(&mut self.prelude);
let value = self.rewrite_value(AIRNode::new(node.id, span, node.kind));
let prelude = std::mem::replace(&mut self.prelude, saved);
let assign = self.assign_temp(temp, value, span);
if prelude.is_empty() {
assign
} else {
let mut stmts = prelude;
stmts.push(assign);
self.node(span, NodeKind::Block { stmts, tail: None })
}
}
}
}
fn rewrite_breaks_to_assign(&mut self, node: AIRNode, temp: &str) -> AIRNode {
let span = node.span;
match node.kind {
NodeKind::Break { value: Some(v) } => {
let value = self.rewrite_value(*v);
let assign = self.assign_temp(temp, value, span);
let brk = self.node(span, NodeKind::Break { value: None });
let mut blk = self.node(
span,
NodeKind::Block {
stmts: vec![assign, brk],
tail: None,
},
);
blk.metadata.insert(
SPLICE_BLOCK_META.to_string(),
bock_air::stubs::Value::Bool(true),
);
blk
}
NodeKind::Loop { .. }
| NodeKind::While { .. }
| NodeKind::For { .. }
| NodeKind::FnDecl { .. }
| NodeKind::Lambda { .. } => self.rewrite(AIRNode::new(node.id, span, node.kind)),
NodeKind::Block { stmts, tail } => {
let mut out: Vec<AIRNode> = Vec::with_capacity(stmts.len());
for s in stmts {
let r = self.rewrite_breaks_to_assign(s, temp);
Self::splice_or_push(&mut out, r);
}
let new_tail = tail.and_then(|t| {
let rewritten = self.rewrite_breaks_to_assign(*t, temp);
Self::splice_or_push(&mut out, rewritten);
None
});
AIRNode::new(
node.id,
span,
NodeKind::Block {
stmts: out,
tail: new_tail,
},
)
}
NodeKind::If {
let_pattern,
condition,
then_block,
else_block,
} => {
let condition = self.rewrite_box(condition);
let then_block = Box::new(self.rewrite_breaks_to_assign(*then_block, temp));
let else_block =
else_block.map(|e| Box::new(self.rewrite_breaks_to_assign(*e, temp)));
AIRNode::new(
node.id,
span,
NodeKind::If {
let_pattern,
condition,
then_block,
else_block,
},
)
}
NodeKind::Match { scrutinee, arms } => {
let scrutinee = self.rewrite_box(scrutinee);
let arms = arms
.into_iter()
.map(|arm| match arm.kind {
NodeKind::MatchArm {
pattern,
guard,
body,
} => {
let body = Box::new(self.rewrite_breaks_to_assign(*body, temp));
AIRNode::new(
arm.id,
arm.span,
NodeKind::MatchArm {
pattern,
guard,
body,
},
)
}
other => AIRNode::new(arm.id, arm.span, other),
})
.collect();
AIRNode::new(node.id, span, NodeKind::Match { scrutinee, arms })
}
other => self.rewrite(AIRNode::new(node.id, span, other)),
}
}
fn splice_or_push(out: &mut Vec<AIRNode>, node: AIRNode) {
if node.metadata.contains_key(SPLICE_BLOCK_META) {
if let NodeKind::Block { stmts, tail } = node.kind {
out.extend(stmts);
if let Some(t) = tail {
out.push(*t);
}
return;
}
}
out.push(node);
}
fn assign_temp(&mut self, temp: &str, value: AIRNode, span: bock_errors::Span) -> AIRNode {
let target = self.node(
span,
NodeKind::Identifier {
name: bock_ast::Ident {
name: temp.to_string(),
span,
},
},
);
self.node(
span,
NodeKind::Assign {
op: bock_ast::AssignOp::Assign,
target: Box::new(target),
value: Box::new(value),
},
)
}
fn rewrite_kind(&mut self, kind: NodeKind, _span: bock_errors::Span) -> NodeKind {
match kind {
NodeKind::Module {
path,
annotations,
imports,
items,
} => NodeKind::Module {
path,
annotations,
imports: imports.into_iter().map(|n| self.rewrite(n)).collect(),
items: items.into_iter().map(|n| self.rewrite(n)).collect(),
},
NodeKind::FnDecl {
annotations,
visibility,
is_async,
name,
generic_params,
params,
return_type,
effect_clause,
where_clause,
body,
} => NodeKind::FnDecl {
annotations,
visibility,
is_async,
name,
generic_params,
params: params.into_iter().map(|p| self.rewrite(p)).collect(),
return_type,
effect_clause,
where_clause,
body: self.rewrite_body(body),
},
NodeKind::ClassDecl {
annotations,
visibility,
name,
generic_params,
base,
traits,
fields,
methods,
} => NodeKind::ClassDecl {
annotations,
visibility,
name,
generic_params,
base,
traits,
fields,
methods: methods.into_iter().map(|m| self.rewrite(m)).collect(),
},
NodeKind::TraitDecl {
annotations,
visibility,
is_platform,
name,
generic_params,
associated_types,
methods,
} => NodeKind::TraitDecl {
annotations,
visibility,
is_platform,
name,
generic_params,
associated_types,
methods: methods.into_iter().map(|m| self.rewrite(m)).collect(),
},
NodeKind::ImplBlock {
annotations,
generic_params,
trait_path,
trait_args,
target,
where_clause,
methods,
} => NodeKind::ImplBlock {
annotations,
generic_params,
trait_path,
trait_args,
target,
where_clause,
methods: methods.into_iter().map(|m| self.rewrite(m)).collect(),
},
NodeKind::EffectDecl {
annotations,
visibility,
name,
generic_params,
components,
operations,
} => NodeKind::EffectDecl {
annotations,
visibility,
name,
generic_params,
components,
operations: operations.into_iter().map(|o| self.rewrite(o)).collect(),
},
NodeKind::ConstDecl {
annotations,
visibility,
name,
ty,
value,
} => NodeKind::ConstDecl {
annotations,
visibility,
name,
ty,
value: self.rewrite_value_box(value),
},
NodeKind::PropertyTest {
name,
bindings,
body,
} => NodeKind::PropertyTest {
name,
bindings,
body: self.rewrite_box(body),
},
NodeKind::LetBinding {
is_mut,
pattern,
ty,
value,
} => NodeKind::LetBinding {
is_mut,
pattern,
ty,
value: self.rewrite_value_box(value),
},
NodeKind::Assign { op, target, value } => NodeKind::Assign {
op,
target,
value: self.rewrite_value_box(value),
},
NodeKind::Return { value } => NodeKind::Return {
value: value.map(|v| self.rewrite_value_box(v)),
},
NodeKind::Break { value } => NodeKind::Break {
value: value.map(|v| self.rewrite_value_box(v)),
},
NodeKind::Call {
callee,
args,
type_args,
} => NodeKind::Call {
callee: self.rewrite_box(callee),
args: args.into_iter().map(|a| self.rewrite_arg(a)).collect(),
type_args,
},
NodeKind::MethodCall {
receiver,
method,
type_args,
args,
} => NodeKind::MethodCall {
receiver: self.rewrite_box(receiver),
method,
type_args,
args: args.into_iter().map(|a| self.rewrite_arg(a)).collect(),
},
NodeKind::Block { stmts, tail } => {
let out_stmts = self.rewrite_stmts(stmts);
let tail = tail.map(|t| self.rewrite_box(t));
NodeKind::Block {
stmts: out_stmts,
tail,
}
}
NodeKind::If {
let_pattern,
condition,
then_block,
else_block,
} => NodeKind::If {
let_pattern,
condition: self.rewrite_box(condition),
then_block: self.rewrite_box(then_block),
else_block: else_block.map(|e| self.rewrite_box(e)),
},
NodeKind::Match { scrutinee, arms } => NodeKind::Match {
scrutinee: self.rewrite_box(scrutinee),
arms: arms.into_iter().map(|a| self.rewrite(a)).collect(),
},
NodeKind::MatchArm {
pattern,
guard,
body,
} => NodeKind::MatchArm {
pattern,
guard,
body: self.rewrite_box(body),
},
NodeKind::For {
pattern,
iterable,
body,
} => NodeKind::For {
pattern,
iterable: self.rewrite_box(iterable),
body: self.rewrite_box(body),
},
NodeKind::While { condition, body } => NodeKind::While {
condition: self.rewrite_box(condition),
body: self.rewrite_box(body),
},
NodeKind::Loop { body } => NodeKind::Loop {
body: self.rewrite_box(body),
},
NodeKind::Guard {
let_pattern,
condition,
else_block,
} => NodeKind::Guard {
let_pattern,
condition: self.rewrite_box(condition),
else_block: self.rewrite_box(else_block),
},
NodeKind::HandlingBlock { handlers, body } => NodeKind::HandlingBlock {
handlers,
body: self.rewrite_box(body),
},
NodeKind::Lambda { params, body } => NodeKind::Lambda {
params,
body: self.rewrite_body(body),
},
NodeKind::BinaryOp { op, left, right } => NodeKind::BinaryOp {
op,
left: self.rewrite_box(left),
right: self.rewrite_box(right),
},
NodeKind::UnaryOp { op, operand } => NodeKind::UnaryOp {
op,
operand: self.rewrite_box(operand),
},
NodeKind::FieldAccess { object, field } => NodeKind::FieldAccess {
object: self.rewrite_box(object),
field,
},
NodeKind::Index { object, index } => NodeKind::Index {
object: self.rewrite_box(object),
index: self.rewrite_box(index),
},
NodeKind::Propagate { expr } => NodeKind::Propagate {
expr: self.rewrite_box(expr),
},
NodeKind::Await { expr } => NodeKind::Await {
expr: self.rewrite_box(expr),
},
NodeKind::Move { expr } => NodeKind::Move {
expr: self.rewrite_box(expr),
},
NodeKind::Borrow { expr } => NodeKind::Borrow {
expr: self.rewrite_box(expr),
},
NodeKind::MutableBorrow { expr } => NodeKind::MutableBorrow {
expr: self.rewrite_box(expr),
},
other => other,
}
}
fn rewrite_arg(&mut self, arg: AirArg) -> AirArg {
AirArg {
label: arg.label,
value: self.rewrite_value(arg.value),
}
}
}
#[must_use]
pub fn loop_needs_break_label(body: &AIRNode) -> bool {
fn arm_has_jump(node: &AIRNode) -> bool {
match &node.kind {
NodeKind::Break { .. } | NodeKind::Continue => true,
NodeKind::For { .. }
| NodeKind::While { .. }
| NodeKind::Loop { .. }
| NodeKind::FnDecl { .. }
| NodeKind::Lambda { .. } => false,
NodeKind::Block { stmts, tail } => {
stmts.iter().any(arm_has_jump) || tail.as_deref().is_some_and(arm_has_jump)
}
NodeKind::If {
then_block,
else_block,
..
} => arm_has_jump(then_block) || else_block.as_deref().is_some_and(arm_has_jump),
NodeKind::Match { arms, .. } => arms
.iter()
.any(|a| matches!(&a.kind, NodeKind::MatchArm { body, .. } if arm_has_jump(body))),
NodeKind::Guard { else_block, .. } => arm_has_jump(else_block),
_ => false,
}
}
fn find(node: &AIRNode) -> bool {
match &node.kind {
NodeKind::For { .. }
| NodeKind::While { .. }
| NodeKind::Loop { .. }
| NodeKind::FnDecl { .. }
| NodeKind::Lambda { .. } => false,
NodeKind::Match { arms, .. } => match_has_statement_arm(arms)
&& arms.iter().any(
|a| matches!(&a.kind, NodeKind::MatchArm { body, .. } if arm_has_jump(body)),
),
NodeKind::Block { stmts, tail } => {
stmts.iter().any(find) || tail.as_deref().is_some_and(find)
}
NodeKind::If {
then_block,
else_block,
..
} => find(then_block) || else_block.as_deref().is_some_and(find),
NodeKind::Guard { else_block, .. } => find(else_block),
_ => false,
}
}
find(body)
}
#[must_use]
pub fn param_binds_self(param: &AIRNode) -> Option<bool> {
let NodeKind::Param { pattern, .. } = ¶m.kind else {
return None;
};
if let NodeKind::BindPat { name, is_mut } = &pattern.kind {
if name.name == "self" {
return Some(*is_mut);
}
}
None
}
#[must_use]
pub fn desugared_self_call<'a>(
callee: &'a AIRNode,
args: &'a [AirArg],
) -> Option<(&'a AIRNode, &'a bock_ast::Ident, &'a [AirArg])> {
let NodeKind::FieldAccess { object, field } = &callee.kind else {
return None;
};
let first = args.first()?;
if first.value.id == object.id {
Some((object.as_ref(), field, &args[1..]))
} else {
None
}
}
pub const READ_ONLY_LIST_METHODS: &[&str] = &[
"len", "length", "count", "is_empty", "get", "contains", "first", "last", "concat", "index_of",
"join",
];
pub const MUTATING_LIST_METHODS: &[&str] = &["push", "append"];
#[must_use]
pub fn raw_recv_kind(node: &AIRNode) -> Option<&str> {
let bock_air::Value::String(tag) =
node.metadata.get(bock_types::checker::RECV_KIND_META_KEY)?
else {
return None;
};
Some(tag.as_str())
}
#[must_use]
pub fn is_associated_call(node: &AIRNode) -> bool {
matches!(
node.metadata.get(bock_air::lower::ASSOC_CALL_META_KEY),
Some(bock_air::Value::Bool(true))
)
}
pub const PRIMITIVE_CONVERSION_TARGETS: &[&str] = &["Int", "Float", "String"];
#[must_use]
pub fn primitive_conversion_call<'a>(
node: &'a AIRNode,
callee: &'a AIRNode,
args: &'a [AirArg],
) -> Option<(&'a str, &'a str, &'a AIRNode)> {
if !is_associated_call(node) {
return None;
}
let NodeKind::FieldAccess { object, field } = &callee.kind else {
return None;
};
let NodeKind::Identifier { name } = &object.kind else {
return None;
};
let target = PRIMITIVE_CONVERSION_TARGETS
.iter()
.copied()
.find(|&p| p == name.name)?;
let method = match field.name.as_str() {
m @ ("from" | "try_from") => m,
_ => return None,
};
let arg = args.first().map(|a| &a.value)?;
if args.len() != 1 {
return None;
}
Some((target, method, arg))
}
#[must_use]
pub fn assoc_fn_def(method: &AIRNode) -> bool {
let NodeKind::FnDecl { params, .. } = &method.kind else {
return false;
};
match params.first() {
Some(first) => param_binds_self(first).is_none(),
None => true,
}
}
#[must_use]
pub fn is_associated_impl_method(method: &AIRNode, effect_ops: &HashMap<String, String>) -> bool {
if !assoc_fn_def(method) {
return false;
}
if let NodeKind::FnDecl { name, .. } = &method.kind {
if effect_ops.contains_key(&name.name) {
return false;
}
}
true
}
#[must_use]
pub fn is_list_concat(node: &AIRNode, left: &AIRNode, right: &AIRNode) -> bool {
let stamped = matches!(
node.metadata.get(bock_types::checker::LIST_CONCAT_META_KEY),
Some(bock_air::Value::Bool(true))
);
let has_list_literal = matches!(left.kind, NodeKind::ListLiteral { .. })
|| matches!(right.kind, NodeKind::ListLiteral { .. });
stamped || has_list_literal
}
#[must_use]
pub fn is_int_arith(node: &AIRNode) -> bool {
matches!(
node.metadata.get(bock_types::checker::INT_ARITH_META_KEY),
Some(bock_air::Value::Bool(true))
)
}
#[must_use]
pub fn is_user_compare(node: &AIRNode) -> bool {
matches!(
node.metadata
.get(bock_types::checker::USER_COMPARE_META_KEY),
Some(bock_air::Value::Bool(true))
)
}
#[must_use]
pub fn user_eq_kind(node: &AIRNode) -> Option<&str> {
match node.metadata.get(bock_types::checker::USER_EQ_META_KEY) {
Some(bock_air::Value::String(kind)) => Some(kind.as_str()),
_ => None,
}
}
#[must_use]
pub fn derives_structural_eq(node: &AIRNode) -> bool {
matches!(
node.metadata.get(bock_types::checker::DERIVE_EQ_META_KEY),
Some(bock_air::Value::Bool(true))
)
}
#[must_use]
pub fn user_compare_variant(op: bock_ast::BinOp) -> Option<(&'static str, bool)> {
use bock_ast::BinOp;
match op {
BinOp::Lt => Some(("Less", true)),
BinOp::Gt => Some(("Greater", true)),
BinOp::Le => Some(("Greater", false)),
BinOp::Ge => Some(("Less", false)),
_ => None,
}
}
#[must_use]
pub fn is_bool_stringify(node: &AIRNode) -> bool {
matches!(
node.metadata
.get(bock_types::checker::BOOL_STRINGIFY_META_KEY),
Some(bock_air::Value::Bool(true))
)
}
#[must_use]
pub fn desugared_list_method<'a>(
call_node: &'a AIRNode,
callee: &'a AIRNode,
args: &'a [AirArg],
) -> Option<(&'a AIRNode, &'a str, &'a [AirArg])> {
if !matches!(raw_recv_kind(call_node), None | Some("List")) {
return None;
}
let (recv, field, rest) = desugared_self_call(callee, args)?;
let method = field.name.as_str();
if READ_ONLY_LIST_METHODS.contains(&method) {
Some((recv, method, rest))
} else {
None
}
}
#[must_use]
pub fn desugared_list_mutating_method<'a>(
call_node: &'a AIRNode,
callee: &'a AIRNode,
args: &'a [AirArg],
) -> Option<(&'a AIRNode, &'a str, &'a [AirArg])> {
if !matches!(raw_recv_kind(call_node), None | Some("List")) {
return None;
}
let (recv, field, rest) = desugared_self_call(callee, args)?;
let method = field.name.as_str();
if MUTATING_LIST_METHODS.contains(&method) {
Some((recv, method, rest))
} else {
None
}
}
pub const INPLACE_LIST_MUTATORS: &[&str] = &["pop", "remove_at", "insert", "reverse", "set"];
#[must_use]
pub fn desugared_list_inplace_mutator<'a>(
call_node: &'a AIRNode,
callee: &'a AIRNode,
args: &'a [AirArg],
) -> Option<(&'a AIRNode, &'a str, &'a [AirArg])> {
let (recv, field, rest) = desugared_self_call(callee, args)?;
let method = field.name.as_str();
if !INPLACE_LIST_MUTATORS.contains(&method) {
return None;
}
let gate_ok = if method == "set" {
matches!(raw_recv_kind(call_node), Some("List"))
} else {
matches!(raw_recv_kind(call_node), None | Some("List"))
};
if gate_ok {
Some((recv, method, rest))
} else {
None
}
}
pub const FUNCTIONAL_LIST_METHODS: &[&str] = &[
"map", "filter", "reduce", "fold", "for_each", "find", "any", "all", "flat_map",
];
#[must_use]
pub fn desugared_list_functional_method<'a>(
call_node: &'a AIRNode,
callee: &'a AIRNode,
args: &'a [AirArg],
) -> Option<(&'a AIRNode, &'a str, &'a [AirArg])> {
if !matches!(raw_recv_kind(call_node), None | Some("List")) {
return None;
}
let (recv, field, rest) = desugared_self_call(callee, args)?;
let method = field.name.as_str();
if FUNCTIONAL_LIST_METHODS.contains(&method) {
Some((recv, method, rest))
} else {
None
}
}
pub const ORDERING_VARIANTS: &[&str] = &["Less", "Equal", "Greater"];
#[must_use]
pub fn ordering_variant(name: &str) -> Option<&'static str> {
ORDERING_VARIANTS.iter().copied().find(|&v| v == name)
}
pub const PRIMITIVE_BRIDGE_METHODS: &[&str] = &["compare", "eq", "to_string", "display"];
#[must_use]
pub fn primitive_recv_kind(node: &AIRNode) -> Option<&str> {
let bock_air::Value::String(tag) =
node.metadata.get(bock_types::checker::RECV_KIND_META_KEY)?
else {
return None;
};
tag.strip_prefix("Primitive:")
}
#[must_use]
pub fn primitive_bridge_call<'a>(
call_node: &'a AIRNode,
callee: &'a AIRNode,
args: &'a [AirArg],
) -> Option<(&'a AIRNode, &'a str, &'a [AirArg], &'a str)> {
let prim = primitive_recv_kind(call_node)?;
let (recv, field, rest) = desugared_self_call(callee, args)?;
let method = field.name.as_str();
if PRIMITIVE_BRIDGE_METHODS.contains(&method) {
Some((recv, method, rest, prim))
} else {
None
}
}
#[must_use]
pub fn trait_bound_recv_kind(node: &AIRNode) -> Option<&str> {
let bock_air::Value::String(tag) =
node.metadata.get(bock_types::checker::RECV_KIND_META_KEY)?
else {
return None;
};
tag.strip_prefix("TraitBound:")
}
#[must_use]
pub fn trait_bound_bridge_call<'a>(
call_node: &'a AIRNode,
callee: &'a AIRNode,
args: &'a [AirArg],
trait_decls: &TraitDeclRegistry,
) -> Option<(&'a AIRNode, &'a str, &'a [AirArg], &'a str)> {
let tr = trait_bound_recv_kind(call_node)?;
if !is_unimplemented_sealed_core_trait(tr, trait_decls) {
return None;
}
let (recv, field, rest) = desugared_self_call(callee, args)?;
let method = field.name.as_str();
if PRIMITIVE_BRIDGE_METHODS.contains(&method) {
Some((recv, method, rest, tr))
} else {
None
}
}
#[must_use]
pub fn is_unimplemented_sealed_core_trait(
trait_name: &str,
trait_decls: &TraitDeclRegistry,
) -> bool {
bock_types::traits::SEALED_CORE_TRAITS.contains(&trait_name)
&& !trait_decls.contains_key(trait_name)
}
#[must_use]
pub fn merge_where_bounds_into_generics(
generic_params: &[bock_ast::GenericParam],
where_clause: &[bock_ast::TypeConstraint],
) -> Vec<bock_ast::GenericParam> {
if where_clause.is_empty() {
return generic_params.to_vec();
}
generic_params
.iter()
.map(|p| {
let mut p = p.clone();
for constraint in where_clause {
if constraint.param.name == p.name.name {
p.bounds.extend(constraint.bounds.iter().cloned());
}
}
p
})
.collect()
}
pub const OPTIONAL_METHODS: &[&str] = &[
"is_some",
"is_none",
"unwrap",
"unwrap_or",
"map",
"flat_map",
];
pub const RESULT_METHODS: &[&str] = &["is_ok", "is_err", "unwrap", "unwrap_or", "map", "map_err"];
#[must_use]
pub fn container_recv_kind(node: &AIRNode) -> Option<&str> {
let bock_air::Value::String(tag) =
node.metadata.get(bock_types::checker::RECV_KIND_META_KEY)?
else {
return None;
};
match tag.as_str() {
"Optional" => Some("Optional"),
"Result" => Some("Result"),
"Map" => Some("Map"),
"Set" => Some("Set"),
_ => None,
}
}
#[must_use]
pub fn desugared_optional_method<'a>(
call_node: &'a AIRNode,
callee: &'a AIRNode,
args: &'a [AirArg],
) -> Option<(&'a AIRNode, &'a str, &'a [AirArg])> {
if container_recv_kind(call_node) != Some("Optional") {
return None;
}
let (recv, field, rest) = desugared_self_call(callee, args)?;
let method = field.name.as_str();
if OPTIONAL_METHODS.contains(&method) {
Some((recv, method, rest))
} else {
None
}
}
#[must_use]
pub fn desugared_result_method<'a>(
call_node: &'a AIRNode,
callee: &'a AIRNode,
args: &'a [AirArg],
) -> Option<(&'a AIRNode, &'a str, &'a [AirArg])> {
if container_recv_kind(call_node) != Some("Result") {
return None;
}
let (recv, field, rest) = desugared_self_call(callee, args)?;
let method = field.name.as_str();
if RESULT_METHODS.contains(&method) {
Some((recv, method, rest))
} else {
None
}
}
pub const MAP_METHODS: &[&str] = &[
"get",
"set",
"delete",
"merge",
"filter",
"keys",
"values",
"entries",
"to_list",
"len",
"length",
"count",
"contains_key",
"is_empty",
"for_each",
];
pub const SET_METHODS: &[&str] = &[
"add",
"remove",
"union",
"intersection",
"difference",
"filter",
"map",
"contains",
"is_subset",
"is_superset",
"len",
"length",
"count",
"is_empty",
"to_list",
"for_each",
];
#[must_use]
pub fn desugared_map_method<'a>(
call_node: &'a AIRNode,
callee: &'a AIRNode,
args: &'a [AirArg],
) -> Option<(&'a AIRNode, &'a str, &'a [AirArg])> {
if container_recv_kind(call_node) != Some("Map") {
return None;
}
let (recv, field, rest) = desugared_self_call(callee, args)?;
let method = field.name.as_str();
if MAP_METHODS.contains(&method) {
Some((recv, method, rest))
} else {
None
}
}
#[must_use]
pub fn desugared_set_method<'a>(
call_node: &'a AIRNode,
callee: &'a AIRNode,
args: &'a [AirArg],
) -> Option<(&'a AIRNode, &'a str, &'a [AirArg])> {
if container_recv_kind(call_node) != Some("Set") {
return None;
}
let (recv, field, rest) = desugared_self_call(callee, args)?;
let method = field.name.as_str();
if SET_METHODS.contains(&method) {
Some((recv, method, rest))
} else {
None
}
}
pub const STRING_METHODS: &[&str] = &[
"len",
"length",
"count",
"byte_len",
"is_empty",
"to_upper",
"to_lower",
"trim",
"contains",
"starts_with",
"ends_with",
"replace",
"split",
];
#[must_use]
pub fn desugared_string_method<'a>(
call_node: &'a AIRNode,
callee: &'a AIRNode,
args: &'a [AirArg],
) -> Option<(&'a AIRNode, &'a str, &'a [AirArg])> {
if primitive_recv_kind(call_node) != Some("String") {
return None;
}
let (recv, field, rest) = desugared_self_call(callee, args)?;
let method = field.name.as_str();
if STRING_METHODS.contains(&method) {
Some((recv, method, rest))
} else {
None
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeywordTarget {
Js,
Ts,
Python,
Rust,
Go,
}
const JS_KEYWORDS: &[&str] = &[
"break",
"case",
"catch",
"class",
"const",
"continue",
"debugger",
"default",
"delete",
"do",
"else",
"export",
"extends",
"false",
"finally",
"for",
"function",
"if",
"import",
"in",
"instanceof",
"new",
"null",
"return",
"super",
"switch",
"this",
"throw",
"true",
"try",
"typeof",
"var",
"void",
"while",
"with",
"yield",
"enum",
"await",
"implements",
"interface",
"let",
"package",
"private",
"protected",
"public",
"static",
];
const TS_EXTRA_KEYWORDS: &[&str] = &[
"abstract",
"as",
"any",
"boolean",
"constructor",
"declare",
"get",
"infer",
"is",
"keyof",
"module",
"namespace",
"never",
"readonly",
"require",
"number",
"object",
"set",
"string",
"symbol",
"type",
"undefined",
"unique",
"unknown",
"from",
"of",
"async",
];
const PYTHON_KEYWORDS: &[&str] = &[
"False", "None", "True", "and", "as", "assert", "async", "await", "break", "class", "continue",
"def", "del", "elif", "else", "except", "finally", "for", "from", "global", "if", "import",
"in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return", "try", "while",
"with", "yield", "match", "case",
];
const RUST_KEYWORDS: &[&str] = &[
"as", "break", "const", "continue", "crate", "dyn", "else", "enum", "extern", "false", "fn",
"for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref",
"return", "self", "Self", "static", "struct", "super", "trait", "true", "type", "unsafe",
"use", "where", "while", "async", "await", "abstract", "become", "box", "do", "final", "macro",
"override", "priv", "typeof", "unsized", "virtual", "yield", "try", "union",
];
const GO_KEYWORDS: &[&str] = &[
"break",
"case",
"chan",
"const",
"continue",
"default",
"defer",
"else",
"fallthrough",
"for",
"func",
"go",
"goto",
"if",
"import",
"interface",
"map",
"package",
"range",
"return",
"select",
"struct",
"switch",
"type",
"var",
];
#[must_use]
pub fn is_target_keyword(name: &str, target: KeywordTarget) -> bool {
match target {
KeywordTarget::Js => JS_KEYWORDS.contains(&name),
KeywordTarget::Ts => JS_KEYWORDS.contains(&name) || TS_EXTRA_KEYWORDS.contains(&name),
KeywordTarget::Python => PYTHON_KEYWORDS.contains(&name),
KeywordTarget::Rust => RUST_KEYWORDS.contains(&name),
KeywordTarget::Go => GO_KEYWORDS.contains(&name),
}
}
#[must_use]
pub fn escape_target_keyword(name: &str, target: KeywordTarget) -> String {
if is_target_keyword(name, target) {
format!("{name}_")
} else {
name.to_string()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VariantPayloadKind {
Unit,
Tuple(usize),
Struct(Vec<String>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnumVariantInfo {
pub enum_name: String,
pub payload: VariantPayloadKind,
}
pub type EnumVariantRegistry = HashMap<String, EnumVariantInfo>;
#[must_use]
pub fn collect_enum_variants(modules: &[(&AIRModule, &Path)]) -> EnumVariantRegistry {
let mut registry = EnumVariantRegistry::new();
seed_builtin_variants(&mut registry);
for (module, _) in modules {
let NodeKind::Module { items, .. } = &module.kind else {
continue;
};
for item in items {
collect_enum_variants_from_item(item, &mut registry);
}
}
registry
}
fn seed_builtin_variants(registry: &mut EnumVariantRegistry) {
registry.insert(
"Some".to_string(),
EnumVariantInfo {
enum_name: "Optional".to_string(),
payload: VariantPayloadKind::Tuple(1),
},
);
registry.insert(
"None".to_string(),
EnumVariantInfo {
enum_name: "Optional".to_string(),
payload: VariantPayloadKind::Unit,
},
);
registry.insert(
"Ok".to_string(),
EnumVariantInfo {
enum_name: "Result".to_string(),
payload: VariantPayloadKind::Tuple(1),
},
);
registry.insert(
"Err".to_string(),
EnumVariantInfo {
enum_name: "Result".to_string(),
payload: VariantPayloadKind::Tuple(1),
},
);
}
fn collect_enum_variants_from_item(item: &AIRNode, registry: &mut EnumVariantRegistry) {
let NodeKind::EnumDecl { name, variants, .. } = &item.kind else {
return;
};
let enum_name = name.name.clone();
for variant in variants {
let NodeKind::EnumVariant {
name: vname,
payload,
} = &variant.kind
else {
continue;
};
let payload = match payload {
EnumVariantPayload::Unit => VariantPayloadKind::Unit,
EnumVariantPayload::Tuple(elems) => VariantPayloadKind::Tuple(elems.len()),
EnumVariantPayload::Struct(fields) => {
VariantPayloadKind::Struct(fields.iter().map(|f| f.name.name.clone()).collect())
}
};
registry.insert(
vname.name.clone(),
EnumVariantInfo {
enum_name: enum_name.clone(),
payload,
},
);
}
}
#[must_use]
pub fn registered_variant<'a>(
registry: &'a EnumVariantRegistry,
path: &bock_ast::TypePath,
) -> Option<&'a EnumVariantInfo> {
let last = path.segments.last()?;
registry.get(&last.name)
}
pub type GenericDeclRegistry = HashMap<String, Vec<bock_ast::GenericParam>>;
#[must_use]
pub fn collect_generic_decls(modules: &[(&AIRModule, &Path)]) -> GenericDeclRegistry {
let mut registry = GenericDeclRegistry::new();
for (module, _) in modules {
let NodeKind::Module { items, .. } = &module.kind else {
continue;
};
for item in items {
collect_generic_decls_from_item(item, &mut registry);
}
}
registry
}
fn collect_generic_decls_from_item(item: &AIRNode, registry: &mut GenericDeclRegistry) {
let (name, generic_params) = match &item.kind {
NodeKind::RecordDecl {
name,
generic_params,
..
}
| NodeKind::EnumDecl {
name,
generic_params,
..
}
| NodeKind::ClassDecl {
name,
generic_params,
..
} => (name, generic_params),
_ => return,
};
registry.insert(name.name.clone(), generic_params.clone());
}
#[must_use]
pub fn collect_exported_type_names(
modules: &[(&AIRModule, &Path)],
) -> std::collections::HashSet<String> {
let mut names = std::collections::HashSet::new();
for (module, _) in modules {
let NodeKind::Module { items, .. } = &module.kind else {
continue;
};
for item in items {
let (visibility, name) = match &item.kind {
NodeKind::RecordDecl {
visibility, name, ..
}
| NodeKind::EnumDecl {
visibility, name, ..
}
| NodeKind::TraitDecl {
visibility, name, ..
}
| NodeKind::ClassDecl {
visibility, name, ..
} => (visibility, name),
_ => continue,
};
if matches!(visibility, bock_ast::Visibility::Public) {
names.insert(name.name.clone());
}
}
}
names
}
#[derive(Debug, Clone)]
pub struct TraitDeclInfo {
pub generic_params: Vec<bock_ast::GenericParam>,
pub methods: Vec<AIRNode>,
}
pub type TraitDeclRegistry = HashMap<String, TraitDeclInfo>;
#[must_use]
pub fn is_default_method(fn_decl: &AIRNode) -> bool {
let NodeKind::FnDecl { body, .. } = &fn_decl.kind else {
return false;
};
match &body.kind {
NodeKind::Block { stmts, tail } => !stmts.is_empty() || tail.is_some(),
_ => true,
}
}
#[must_use]
pub fn fn_decl_name(fn_decl: &AIRNode) -> Option<&str> {
if let NodeKind::FnDecl { name, .. } = &fn_decl.kind {
Some(name.name.as_str())
} else {
None
}
}
#[must_use]
fn type_node_mentions_self(node: &AIRNode) -> bool {
match &node.kind {
NodeKind::TypeSelf => true,
NodeKind::TypeOptional { inner } => type_node_mentions_self(inner),
NodeKind::TypeTuple { elems } => elems.iter().any(type_node_mentions_self),
NodeKind::TypeFunction { params, ret, .. } => {
params.iter().any(type_node_mentions_self) || type_node_mentions_self(ret)
}
NodeKind::TypeNamed { args, .. } => args.iter().any(type_node_mentions_self),
_ => false,
}
}
#[must_use]
pub fn trait_uses_self_operand(trait_info: &TraitDeclInfo) -> bool {
trait_info.methods.iter().any(|m| {
let NodeKind::FnDecl {
params,
return_type,
..
} = &m.kind
else {
return false;
};
let param_self = params
.iter()
.skip(1)
.filter_map(|p| {
if let NodeKind::Param { ty: Some(t), .. } = &p.kind {
Some(t.as_ref())
} else {
None
}
})
.any(type_node_mentions_self);
let ret_self = return_type.as_deref().is_some_and(type_node_mentions_self);
param_self || ret_self
})
}
#[must_use]
pub fn collect_trait_decls(modules: &[(&AIRModule, &Path)]) -> TraitDeclRegistry {
let mut registry = TraitDeclRegistry::new();
for (module, _) in modules {
let NodeKind::Module { items, .. } = &module.kind else {
continue;
};
for item in items {
if let NodeKind::TraitDecl {
name,
generic_params,
methods,
..
} = &item.kind
{
registry.insert(
name.name.clone(),
TraitDeclInfo {
generic_params: generic_params.clone(),
methods: methods.clone(),
},
);
}
}
}
registry
}
#[must_use]
pub fn inherited_default_methods(
registry: &TraitDeclRegistry,
trait_path: &bock_ast::TypePath,
impl_methods: &[AIRNode],
) -> Vec<AIRNode> {
let Some(trait_name) = trait_path.segments.last().map(|s| s.name.as_str()) else {
return Vec::new();
};
let Some(info) = registry.get(trait_name) else {
return Vec::new();
};
let overridden: std::collections::HashSet<&str> =
impl_methods.iter().filter_map(fn_decl_name).collect();
info.methods
.iter()
.filter(|m| is_default_method(m))
.filter(|m| fn_decl_name(m).is_some_and(|n| !overridden.contains(n)))
.cloned()
.collect()
}
#[must_use]
pub fn impl_has_instance_method(
impl_block: &AIRNode,
effect_ops: &HashMap<String, String>,
) -> bool {
let NodeKind::ImplBlock { methods, .. } = &impl_block.kind else {
return false;
};
methods
.iter()
.any(|m| !is_associated_impl_method(m, effect_ops))
}
#[must_use]
pub fn collect_record_field_names<F>(
module: &AIRNode,
cased: F,
) -> std::collections::HashSet<String>
where
F: Fn(&str) -> String,
{
let mut names = std::collections::HashSet::new();
if let NodeKind::Module { items, .. } = &module.kind {
for item in items {
if let NodeKind::RecordDecl { fields, .. } | NodeKind::ClassDecl { fields, .. } =
&item.kind
{
for f in fields {
names.insert(cased(&f.name.name));
}
}
}
}
names
}
#[must_use]
pub fn disambiguate_method_name(
cased_name: String,
field_names: &std::collections::HashSet<String>,
suffix: &str,
) -> String {
if field_names.contains(&cased_name) {
format!("{cased_name}{suffix}")
} else {
cased_name
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn disambiguate_method_name_suffixes_only_on_collision() {
let mut fields = std::collections::HashSet::new();
fields.insert("message".to_string());
fields.insert("Message".to_string());
assert_eq!(
disambiguate_method_name("render".to_string(), &fields, "Method"),
"render"
);
assert_eq!(
disambiguate_method_name("message".to_string(), &fields, "Method"),
"messageMethod"
);
assert_eq!(
disambiguate_method_name("Message".to_string(), &fields, "Method"),
"MessageMethod"
);
assert_eq!(
disambiguate_method_name("message".to_string(), &fields, "_method"),
"message_method"
);
}
#[test]
fn merge_where_bounds_folds_constraints_onto_matching_param() {
use bock_ast::{GenericParam, Ident, TypeConstraint, TypePath};
use bock_errors::{FileId, Span};
fn span() -> Span {
Span {
file: FileId(0),
start: 0,
end: 0,
}
}
fn ident(name: &str) -> Ident {
Ident {
span: span(),
name: name.to_string(),
}
}
fn type_path(name: &str) -> TypePath {
TypePath {
segments: vec![ident(name)],
span: span(),
}
}
fn param(name: &str, bounds: Vec<TypePath>) -> GenericParam {
GenericParam {
id: 0,
span: span(),
name: ident(name),
bounds,
}
}
fn constraint(param: &str, bounds: Vec<TypePath>) -> TypeConstraint {
TypeConstraint {
id: 0,
span: span(),
param: ident(param),
bounds,
}
}
let params = vec![param("T", vec![])];
let merged = merge_where_bounds_into_generics(¶ms, &[]);
assert_eq!(merged, params);
let params = vec![param("T", vec![]), param("U", vec![])];
let wc = vec![constraint("T", vec![type_path("Ranked")])];
let merged = merge_where_bounds_into_generics(¶ms, &wc);
assert_eq!(
merged[0]
.bounds
.iter()
.map(|b| &b.segments[0].name)
.collect::<Vec<_>>(),
vec!["Ranked"]
);
assert!(merged[1].bounds.is_empty());
let params = vec![param("T", vec![type_path("Show")])];
let wc = vec![constraint("T", vec![type_path("Ranked")])];
let merged = merge_where_bounds_into_generics(¶ms, &wc);
assert_eq!(
merged[0]
.bounds
.iter()
.map(|b| &b.segments[0].name)
.collect::<Vec<_>>(),
vec!["Show", "Ranked"]
);
}
#[test]
fn collect_record_field_names_unions_records_and_classes() {
use bock_ast::{Ident, RecordDeclField, TypeExpr, TypePath, Visibility};
use bock_errors::{FileId, Span};
fn span() -> Span {
Span {
file: FileId(0),
start: 0,
end: 0,
}
}
fn ident(name: &str) -> Ident {
Ident {
name: name.to_string(),
span: span(),
}
}
fn ty() -> TypeExpr {
TypeExpr::Named {
id: 0,
span: span(),
path: TypePath {
segments: vec![ident("String")],
span: span(),
},
args: vec![],
}
}
fn field(name: &str) -> RecordDeclField {
RecordDeclField {
id: 0,
span: span(),
name: ident(name),
ty: ty(),
default: None,
}
}
let record = AIRNode::new(
1,
span(),
NodeKind::RecordDecl {
annotations: vec![],
visibility: Visibility::Public,
name: ident("SimpleError"),
generic_params: vec![],
fields: vec![field("message")],
},
);
let class = AIRNode::new(
2,
span(),
NodeKind::ClassDecl {
annotations: vec![],
visibility: Visibility::Public,
name: ident("Handler"),
generic_params: vec![],
base: None,
traits: vec![],
fields: vec![field("state")],
methods: vec![],
},
);
let module = AIRNode::new(
0,
span(),
NodeKind::Module {
path: None,
annotations: vec![],
imports: vec![],
items: vec![record, class],
},
);
let names = collect_record_field_names(&module, |n| n.to_string());
assert!(names.contains("message"));
assert!(names.contains("state"));
assert_eq!(names.len(), 2);
}
#[test]
fn output_file_stores_path_and_content() {
let f = OutputFile {
path: PathBuf::from("main.js"),
content: "console.log('hello');".into(),
source_map: None,
};
assert_eq!(f.path, PathBuf::from("main.js"));
assert!(f.content.contains("console.log"));
}
#[test]
fn generated_code_with_no_source_map() {
let code = GeneratedCode {
files: vec![OutputFile {
path: PathBuf::from("out.py"),
content: "print('hello')".into(),
source_map: None,
}],
};
assert_eq!(code.files.len(), 1);
assert!(code.files[0].source_map.is_none());
}
#[test]
fn derive_output_path_strips_src_prefix() {
let js = TargetProfile::javascript();
assert_eq!(
derive_output_path(Path::new("src/main.bock"), &js),
PathBuf::from("main.js")
);
assert_eq!(
derive_output_path(Path::new("src/utils/parse.bock"), &js),
PathBuf::from("utils/parse.js")
);
assert_eq!(
derive_output_path(Path::new("src/api/v1/handler.bock"), &js),
PathBuf::from("api/v1/handler.js")
);
}
#[test]
fn derive_output_path_preserves_paths_without_src_prefix() {
let py = TargetProfile::python();
assert_eq!(
derive_output_path(Path::new("main.bock"), &py),
PathBuf::from("main.py")
);
assert_eq!(
derive_output_path(Path::new("lib/foo.bock"), &py),
PathBuf::from("lib/foo.py")
);
}
#[test]
fn derive_output_path_normalizes_leading_curdir() {
let js = TargetProfile::javascript();
assert_eq!(
derive_output_path(Path::new("./src/main.bock"), &js),
PathBuf::from("main.js")
);
assert_eq!(
derive_output_path(Path::new("./main.bock"), &js),
PathBuf::from("main.js")
);
assert_eq!(
derive_output_path(Path::new("./src/utils/parse.bock"), &js),
PathBuf::from("utils/parse.js")
);
}
#[test]
fn derive_output_path_uses_target_extension() {
let path = Path::new("src/main.bock");
assert_eq!(
derive_output_path(path, &TargetProfile::javascript()),
PathBuf::from("main.js")
);
assert_eq!(
derive_output_path(path, &TargetProfile::typescript()),
PathBuf::from("main.ts")
);
assert_eq!(
derive_output_path(path, &TargetProfile::python()),
PathBuf::from("main.py")
);
assert_eq!(
derive_output_path(path, &TargetProfile::rust()),
PathBuf::from("main.rs")
);
assert_eq!(
derive_output_path(path, &TargetProfile::go()),
PathBuf::from("main.go")
);
}
#[test]
fn esm_relative_specifier_from_entry_root() {
assert_eq!(
esm_relative_specifier("", "core.option", "js"),
"./core/option.js"
);
assert_eq!(esm_relative_specifier("", "helper", "js"), "./helper.js");
}
#[test]
fn esm_relative_specifier_between_nested_modules() {
assert_eq!(
esm_relative_specifier("helper", "core.option", "ts"),
"./core/option.ts"
);
assert_eq!(
esm_relative_specifier("core.option", "core.compare", "js"),
"./compare.js"
);
assert_eq!(
esm_relative_specifier("a.b.deep", "helper", "js"),
"../../helper.js"
);
assert_eq!(
esm_relative_specifier("a.b.deep", "a.c.thing", "js"),
"../c/thing.js"
);
}
#[test]
fn source_map_default_is_empty() {
let sm = SourceMap::default();
assert!(sm.entries.is_empty());
assert!(sm.mappings.is_empty());
assert!(sm.sources.is_empty());
}
#[test]
fn byte_to_line_col_basic() {
let s = "abc\ndef\nghi";
assert_eq!(byte_to_line_col(s, 0), (1, 1));
assert_eq!(byte_to_line_col(s, 3), (1, 4));
assert_eq!(byte_to_line_col(s, 4), (2, 1));
assert_eq!(byte_to_line_col(s, 8), (3, 1));
}
#[test]
fn resolve_positions_fills_line_col() {
let mut sm = SourceMap {
mappings: vec![SourceMapping {
gen_line: 1,
gen_col: 1,
src_line: 0,
src_col: 0,
src_offset: 4,
src_file_id: 0,
}],
..Default::default()
};
sm.resolve_positions(&["abc\ndef"]);
assert_eq!(sm.mappings[0].src_line, 2);
assert_eq!(sm.mappings[0].src_col, 1);
}
#[test]
fn vlq_encodes_known_values() {
let mut s = String::new();
vlq_encode(&mut s, 0);
assert_eq!(s, "A");
s.clear();
vlq_encode(&mut s, 1);
assert_eq!(s, "C");
s.clear();
vlq_encode(&mut s, -1);
assert_eq!(s, "D");
s.clear();
vlq_encode(&mut s, 16);
assert_eq!(s, "gB");
}
#[test]
fn source_map_v3_json_contains_required_fields() {
let mut sm = SourceMap {
generated_file: "output.js".into(),
..Default::default()
};
sm.sources.push(SourceInfo {
path: "main.bock".into(),
content: Some("let x = 1\n".into()),
});
sm.mappings.push(SourceMapping {
gen_line: 1,
gen_col: 1,
src_line: 1,
src_col: 1,
src_offset: 0,
src_file_id: 0,
});
let json = sm.to_source_map_v3_json();
assert!(json.contains("\"version\":3"));
assert!(json.contains("\"file\":\"output.js\""));
assert!(json.contains("\"sources\":[\"main.bock\"]"));
assert!(json.contains("\"mappings\":"));
}
use bock_air::AIRNode;
use bock_ast::{Ident, Visibility};
use bock_errors::{FileId, Span};
fn dummy_span() -> Span {
Span {
file: FileId(0),
start: 0,
end: 0,
}
}
fn ident(name: &str) -> Ident {
Ident {
name: name.to_string(),
span: dummy_span(),
}
}
fn fn_decl(name: &str) -> AIRNode {
let body = AIRNode::new(
1,
dummy_span(),
NodeKind::Block {
stmts: vec![],
tail: None,
},
);
AIRNode::new(
0,
dummy_span(),
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Public,
is_async: false,
name: ident(name),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
)
}
fn module_with(items: Vec<AIRNode>) -> AIRNode {
AIRNode::new(
0,
dummy_span(),
NodeKind::Module {
path: None,
annotations: vec![],
imports: vec![],
items,
},
)
}
fn module_named(path: &str, uses: &[&str], items: Vec<AIRNode>) -> AIRNode {
use bock_ast::ModulePath;
let module_path = ModulePath {
segments: path.split('.').map(ident).collect(),
span: dummy_span(),
};
let imports = uses
.iter()
.enumerate()
.map(|(i, dep)| {
AIRNode::new(
100 + i as u32,
dummy_span(),
NodeKind::ImportDecl {
path: bock_ast::ModulePath {
segments: dep.split('.').map(ident).collect(),
span: dummy_span(),
},
items: bock_ast::ImportItems::Glob,
},
)
})
.collect();
AIRNode::new(
0,
dummy_span(),
NodeKind::Module {
path: Some(module_path),
annotations: vec![],
imports,
items,
},
)
}
fn test_fn_decl(name: &str, body: AIRNode) -> AIRNode {
use bock_ast::{Annotation, Visibility};
let annotation = Annotation {
id: 0,
name: ident("test"),
args: vec![],
span: dummy_span(),
};
AIRNode::new(
0,
dummy_span(),
NodeKind::FnDecl {
annotations: vec![annotation],
visibility: Visibility::Private,
is_async: false,
name: ident(name),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
)
}
fn identifier(id: u32, name: &str) -> AIRNode {
AIRNode::new(id, dummy_span(), NodeKind::Identifier { name: ident(name) })
}
fn call(id: u32, callee: AIRNode, args: Vec<AIRNode>) -> AIRNode {
AIRNode::new(
id,
dummy_span(),
NodeKind::Call {
callee: Box::new(callee),
args: args
.into_iter()
.map(|value| AirArg { label: None, value })
.collect(),
type_args: vec![],
},
)
}
fn assertion(method: &str, actual: AIRNode, expected: Option<AIRNode>) -> AIRNode {
let expect_call = call(10, identifier(11, "expect"), vec![actual]);
let field = AIRNode::new(
12,
dummy_span(),
NodeKind::FieldAccess {
object: Box::new(expect_call.clone()),
field: ident(method),
},
);
let mut args = vec![expect_call];
if let Some(e) = expected {
args.push(e);
}
call(13, field, args)
}
#[test]
fn fn_is_test_detects_test_annotation() {
let body = AIRNode::new(
1,
dummy_span(),
NodeKind::Block {
stmts: vec![],
tail: None,
},
);
assert!(fn_is_test(&test_fn_decl("t", body)));
assert!(!fn_is_test(&fn_decl("not_a_test")));
}
#[test]
fn collect_test_fns_finds_annotated_functions() {
let body = AIRNode::new(
1,
dummy_span(),
NodeKind::Block {
stmts: vec![],
tail: None,
},
);
let m = module_named(
"main",
&[],
vec![
fn_decl("main"),
test_fn_decl("test_a", body.clone()),
fn_decl("helper"),
test_fn_decl("test_b", body),
],
);
let p = std::path::Path::new("x.bock");
let modules = [(&m, p)];
let tests = collect_test_fns(&modules);
assert_eq!(tests.len(), 2);
let names: Vec<&str> = tests
.iter()
.map(|(n, _)| match &n.kind {
NodeKind::FnDecl { name, .. } => name.name.as_str(),
_ => "?",
})
.collect();
assert_eq!(names, vec!["test_a", "test_b"]);
assert_eq!(tests[0].1, "main");
}
#[test]
fn classify_assertion_recognizes_equal() {
let stmt = assertion("to_equal", identifier(1, "x"), Some(identifier(2, "y")));
let (kind, actual, expected) = classify_assertion(&stmt).expect("should classify");
assert_eq!(kind, TestAssertion::Equal);
assert!(matches!(&actual.kind, NodeKind::Identifier { name } if name.name == "x"));
let expected = expected.expect("equal has an expected operand");
assert!(matches!(&expected.kind, NodeKind::Identifier { name } if name.name == "y"));
}
#[test]
fn classify_assertion_recognizes_nullary_predicates() {
for (method, expected_kind) in [
("to_be_true", TestAssertion::BeTrue),
("to_be_false", TestAssertion::BeFalse),
("to_be_some", TestAssertion::BeSome),
("to_be_none", TestAssertion::BeNone),
("to_be_ok", TestAssertion::BeOk),
("to_be_err", TestAssertion::BeErr),
] {
let stmt = assertion(method, identifier(1, "v"), None);
let (kind, _actual, expected) =
classify_assertion(&stmt).unwrap_or_else(|| panic!("classify {method}"));
assert_eq!(kind, expected_kind, "method {method}");
assert!(expected.is_none(), "{method} takes no expected operand");
}
}
#[test]
fn classify_assertion_rejects_non_assertions() {
let plain = call(1, identifier(2, "do_thing"), vec![]);
assert!(classify_assertion(&plain).is_none());
let other = {
let recv = call(3, identifier(4, "build"), vec![]);
let field = AIRNode::new(
5,
dummy_span(),
NodeKind::FieldAccess {
object: Box::new(recv.clone()),
field: ident("to_equal"),
},
);
call(6, field, vec![recv, identifier(7, "z")])
};
assert!(classify_assertion(&other).is_none());
let unknown = assertion("to_be_weird", identifier(8, "v"), None);
assert!(classify_assertion(&unknown).is_none());
}
#[test]
fn reachable_modules_prunes_unused_prelude_modules() {
let core_a = module_named("core.compare", &[], vec![]);
let core_b = module_named("core.convert", &["core.compare"], vec![]);
let main_m = module_named("main", &[], vec![fn_decl("main")]);
let p = std::path::Path::new("x.bock");
let modules = [(&core_a, p), (&core_b, p), (&main_m, p)];
let got = reachable_modules(&modules);
assert_eq!(got.len(), 1, "only the entry module should be reachable");
assert!(module_declares_main_fn(got[0].0));
}
#[test]
fn reachable_modules_includes_transitive_use_targets() {
let helper = module_named("helper", &[], vec![fn_decl("h")]);
let util = module_named("util", &["helper"], vec![fn_decl("u")]);
let unused = module_named("unused", &[], vec![fn_decl("x")]);
let main_m = module_named("main", &["util"], vec![fn_decl("main")]);
let p = std::path::Path::new("x.bock");
let modules = [(&helper, p), (&util, p), (&unused, p), (&main_m, p)];
let got = reachable_modules(&modules);
let paths: Vec<String> = got
.iter()
.map(|(m, _)| {
let NodeKind::Module { path: Some(pp), .. } = &m.kind else {
return String::new();
};
pp.segments
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>()
.join(".")
})
.collect();
assert!(paths.contains(&"main".to_string()));
assert!(paths.contains(&"util".to_string()));
assert!(paths.contains(&"helper".to_string()));
assert!(!paths.contains(&"unused".to_string()), "got: {paths:?}");
let pos = |name: &str| paths.iter().position(|x| x == name).unwrap();
assert!(pos("helper") < pos("util"));
assert!(pos("util") < pos("main"));
}
#[test]
fn reachable_modules_order_is_input_order_independent() {
let leaf = module_named("z.leaf", &[], vec![fn_decl("l")]);
let a = module_named("core.a", &["z.leaf"], vec![fn_decl("a")]);
let b = module_named("core.b", &[], vec![fn_decl("b")]);
let c = module_named("core.c", &[], vec![fn_decl("c")]);
let main_m = module_named(
"main",
&["core.a", "core.b", "core.c"],
vec![fn_decl("main")],
);
let p = std::path::Path::new("x.bock");
let names = |got: &[(&AIRModule, &std::path::Path)]| -> Vec<String> {
got.iter()
.filter_map(|(m, _)| {
if let NodeKind::Module { path: Some(pp), .. } = &m.kind {
Some(
pp.segments
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>()
.join("."),
)
} else {
None
}
})
.collect()
};
let perm1 = [(&leaf, p), (&a, p), (&b, p), (&c, p), (&main_m, p)];
let perm2 = [(&c, p), (&main_m, p), (&b, p), (&leaf, p), (&a, p)];
let perm3 = [(&main_m, p), (&c, p), (&b, p), (&a, p), (&leaf, p)];
let o1 = names(&reachable_modules(&perm1));
let o2 = names(&reachable_modules(&perm2));
let o3 = names(&reachable_modules(&perm3));
assert_eq!(o1, o2, "module order must not depend on input order");
assert_eq!(o1, o3, "module order must not depend on input order");
assert_eq!(o1.len(), 5, "got: {o1:?}");
let pos = |name: &str| o1.iter().position(|x| x == name).unwrap();
assert!(pos("z.leaf") < pos("core.a"), "got: {o1:?}");
assert!(pos("core.a") < pos("main"), "got: {o1:?}");
assert!(pos("core.b") < pos("main"), "got: {o1:?}");
assert!(pos("core.c") < pos("main"), "got: {o1:?}");
assert_eq!(o1.last().map(String::as_str), Some("main"), "got: {o1:?}");
}
#[test]
fn module_declares_main_detects_top_level_main() {
let m = module_with(vec![fn_decl("helper"), fn_decl("main")]);
assert!(module_declares_main_fn(&m));
}
#[test]
fn module_declares_main_returns_false_when_absent() {
let m = module_with(vec![fn_decl("helper"), fn_decl("other")]);
assert!(!module_declares_main_fn(&m));
}
#[test]
fn module_declares_main_returns_false_for_empty_module() {
let m = module_with(vec![]);
assert!(!module_declares_main_fn(&m));
}
fn n(id: u32, kind: NodeKind) -> AIRNode {
AIRNode::new(id, dummy_span(), kind)
}
fn match_arm(id: u32, body: AIRNode) -> AIRNode {
n(
id,
NodeKind::MatchArm {
pattern: Box::new(n(id + 1, NodeKind::WildcardPat)),
guard: None,
body: Box::new(body),
},
)
}
#[test]
fn node_is_statement_classifies_control_flow() {
assert!(node_is_statement(&n(1, NodeKind::Break { value: None })));
assert!(node_is_statement(&n(1, NodeKind::Continue)));
assert!(node_is_statement(&n(1, NodeKind::Return { value: None })));
assert!(!node_is_statement(&n(
1,
NodeKind::Literal {
lit: bock_ast::Literal::Int("1".into())
}
)));
}
fn block_with_tail(id: u32, tail: AIRNode) -> AIRNode {
n(
id,
NodeKind::Block {
stmts: vec![],
tail: Some(Box::new(tail)),
},
)
}
fn int_lit(id: u32) -> AIRNode {
n(
id,
NodeKind::Literal {
lit: bock_ast::Literal::Int("1".into()),
},
)
}
fn if_node(id: u32, then_block: AIRNode, else_block: Option<AIRNode>) -> AIRNode {
n(
id,
NodeKind::If {
let_pattern: None,
condition: Box::new(n(id + 100, NodeKind::Placeholder)),
then_block: Box::new(then_block),
else_block: else_block.map(Box::new),
},
)
}
#[test]
fn node_is_statement_classifies_no_else_if_as_statement() {
let no_else = if_node(
1,
block_with_tail(2, n(3, NodeKind::Return { value: None })),
None,
);
assert!(node_is_statement(&no_else));
let no_else_break = if_node(
10,
block_with_tail(11, n(12, NodeKind::Break { value: None })),
None,
);
assert!(node_is_statement(&no_else_break));
}
#[test]
fn node_is_statement_classifies_all_statement_if_else_as_statement() {
let stmt_both = if_node(
1,
block_with_tail(2, n(3, NodeKind::Return { value: None })),
Some(block_with_tail(4, n(5, NodeKind::Break { value: None }))),
);
assert!(node_is_statement(&stmt_both));
}
#[test]
fn node_is_statement_leaves_value_if_else_an_expression() {
let value_if = if_node(
1,
block_with_tail(2, int_lit(3)),
Some(block_with_tail(4, int_lit(5))),
);
assert!(!node_is_statement(&value_if));
assert!(!arm_body_is_statement(&value_if));
}
#[test]
fn node_is_statement_leaves_mixed_if_else_an_expression() {
let mixed = if_node(
1,
block_with_tail(2, n(3, NodeKind::Return { value: None })),
Some(block_with_tail(4, int_lit(5))),
);
assert!(!node_is_statement(&mixed));
}
#[test]
fn node_is_statement_handles_else_if_chains() {
let inner = if_node(
20,
block_with_tail(21, n(22, NodeKind::Break { value: None })),
None,
);
let chain = if_node(
1,
block_with_tail(2, n(3, NodeKind::Return { value: None })),
Some(inner),
);
assert!(node_is_statement(&chain));
let value_inner = if_node(
30,
block_with_tail(31, int_lit(32)),
Some(block_with_tail(33, int_lit(34))),
);
let mixed_chain = if_node(
40,
block_with_tail(41, n(42, NodeKind::Return { value: None })),
Some(value_inner),
);
assert!(!node_is_statement(&mixed_chain));
}
#[test]
fn arm_body_is_statement_for_block_with_statement_tail() {
let block_tail_break = n(
1,
NodeKind::Block {
stmts: vec![],
tail: Some(Box::new(n(2, NodeKind::Break { value: None }))),
},
);
assert!(arm_body_is_statement(&block_tail_break));
let empty = n(
3,
NodeKind::Block {
stmts: vec![],
tail: None,
},
);
assert!(arm_body_is_statement(&empty));
}
#[test]
fn match_has_statement_arm_detects_break() {
let arms = vec![
match_arm(10, n(12, NodeKind::Break { value: None })),
match_arm(
20,
n(
22,
NodeKind::Literal {
lit: bock_ast::Literal::Int("0".into()),
},
),
),
];
assert!(match_has_statement_arm(&arms));
let value_arms = vec![match_arm(
30,
n(
32,
NodeKind::Literal {
lit: bock_ast::Literal::Int("0".into()),
},
),
)];
assert!(!match_has_statement_arm(&value_arms));
}
fn ctor_path(name: &str) -> bock_ast::TypePath {
bock_ast::TypePath {
segments: vec![ident(name)],
span: dummy_span(),
}
}
fn arm_with(id: u32, pattern: AIRNode, guard: Option<AIRNode>) -> AIRNode {
n(
id,
NodeKind::MatchArm {
pattern: Box::new(pattern),
guard: guard.map(Box::new),
body: Box::new(int_lit(id + 100)),
},
)
}
#[test]
fn match_needs_ifchain_keeps_switch_fast_path_for_simple_matches() {
let bind_arms = vec![
arm_with(
1,
n(
2,
NodeKind::BindPat {
name: ident("x"),
is_mut: false,
},
),
None,
),
arm_with(3, n(4, NodeKind::WildcardPat), None),
];
assert!(!match_needs_ifchain(&bind_arms));
let flat_ctor = vec![arm_with(
10,
n(
11,
NodeKind::ConstructorPat {
path: ctor_path("Some"),
fields: vec![n(
12,
NodeKind::BindPat {
name: ident("x"),
is_mut: false,
},
)],
},
),
None,
)];
assert!(!match_needs_ifchain(&flat_ctor));
}
#[test]
fn match_needs_ifchain_detects_guard() {
let arms = vec![arm_with(1, n(2, NodeKind::WildcardPat), Some(int_lit(3)))];
assert!(match_needs_ifchain(&arms));
}
#[test]
fn match_needs_ifchain_detects_or_and_tuple() {
let or_arm = vec![arm_with(
1,
n(
2,
NodeKind::OrPat {
alternatives: vec![int_lit(3), int_lit(4)],
},
),
None,
)];
assert!(match_needs_ifchain(&or_arm));
let tuple_arm = vec![arm_with(
10,
n(
11,
NodeKind::TuplePat {
elems: vec![
n(
12,
NodeKind::BindPat {
name: ident("a"),
is_mut: false,
},
),
n(
13,
NodeKind::BindPat {
name: ident("b"),
is_mut: false,
},
),
],
},
),
None,
)];
assert!(match_needs_ifchain(&tuple_arm));
}
#[test]
fn match_needs_ifchain_detects_nested_constructor() {
let nested = vec![arm_with(
1,
n(
2,
NodeKind::ConstructorPat {
path: ctor_path("Some"),
fields: vec![n(
3,
NodeKind::ConstructorPat {
path: ctor_path("Ok"),
fields: vec![n(
4,
NodeKind::BindPat {
name: ident("v"),
is_mut: false,
},
)],
},
)],
},
),
None,
)];
assert!(match_needs_ifchain(&nested));
}
#[test]
fn match_needs_ifchain_detects_list_pattern() {
let empty = vec![arm_with(
1,
n(
2,
NodeKind::ListPat {
elems: vec![],
rest: None,
},
),
None,
)];
assert!(match_needs_ifchain(&empty));
let head_rest = vec![arm_with(
10,
n(
11,
NodeKind::ListPat {
elems: vec![n(
12,
NodeKind::BindPat {
name: ident("first"),
is_mut: false,
},
)],
rest: Some(Box::new(n(
13,
NodeKind::BindPat {
name: ident("rest"),
is_mut: false,
},
))),
},
),
None,
)];
assert!(match_needs_ifchain(&head_rest));
}
#[test]
fn match_needs_ifchain_detects_range_pattern() {
let range = vec![arm_with(
1,
n(
2,
NodeKind::RangePat {
lo: Box::new(int_lit(3)),
hi: Box::new(int_lit(4)),
inclusive: false,
},
),
None,
)];
assert!(match_needs_ifchain(&range));
}
#[test]
fn desugared_self_call_matches_shared_receiver_id() {
let recv = n(5, NodeKind::Identifier { name: ident("p") });
let callee = n(
6,
NodeKind::FieldAccess {
object: Box::new(recv.clone()),
field: ident("m"),
},
);
let args = vec![
AirArg {
label: None,
value: recv,
},
AirArg {
label: None,
value: n(7, NodeKind::Identifier { name: ident("x") }),
},
];
let got = desugared_self_call(&callee, &args).expect("should match");
assert_eq!(got.1.name, "m");
assert_eq!(got.2.len(), 1);
let p1 = n(8, NodeKind::Identifier { name: ident("p") });
let p2 = n(9, NodeKind::Identifier { name: ident("p") });
let callee2 = n(
10,
NodeKind::FieldAccess {
object: Box::new(p1),
field: ident("f"),
},
);
let args2 = vec![AirArg {
label: None,
value: p2,
}];
assert!(desugared_self_call(&callee2, &args2).is_none());
}
fn desugared_call(method: &str, extra: Vec<AIRNode>) -> (AIRNode, Vec<AirArg>, AIRNode) {
let recv = n(
5,
NodeKind::Identifier {
name: ident("nums"),
},
);
let callee = n(
6,
NodeKind::FieldAccess {
object: Box::new(recv.clone()),
field: ident(method),
},
);
let mut args = vec![AirArg {
label: None,
value: recv,
}];
args.extend(extra.into_iter().map(|value| AirArg { label: None, value }));
let call_node = n(
8,
NodeKind::Call {
callee: Box::new(callee.clone()),
args: args.clone(),
type_args: vec![],
},
);
(callee, args, call_node)
}
#[test]
fn desugared_list_method_matches_read_only_builtins() {
for &m in READ_ONLY_LIST_METHODS {
let extra = match m {
"get" | "contains" | "index_of" | "concat" | "join" => {
vec![n(7, NodeKind::Identifier { name: ident("x") })]
}
_ => vec![],
};
let n_extra = extra.len();
let (callee, args, call_node) = desugared_call(m, extra);
let (recv, got_method, rest) =
desugared_list_method(&call_node, &callee, &args).expect("should match");
assert_eq!(got_method, m);
assert!(matches!(&recv.kind, NodeKind::Identifier { name } if name.name == "nums"));
assert_eq!(rest.len(), n_extra);
}
}
#[test]
fn desugared_list_method_rejects_mutating_and_unknown_methods() {
for &m in &["push", "pop", "insert", "remove", "clear", "frobnicate"] {
let (callee, args, call_node) = desugared_call(m, vec![]);
assert!(
desugared_list_method(&call_node, &callee, &args).is_none(),
"{m} should not be recognised as a read-only List method"
);
}
}
#[test]
fn desugared_list_inplace_mutator_matches_dq30_methods() {
for &m in INPLACE_LIST_MUTATORS {
let extra = match m {
"pop" | "reverse" => vec![],
"remove_at" => vec![n(7, NodeKind::Identifier { name: ident("i") })],
_ => vec![
n(7, NodeKind::Identifier { name: ident("i") }),
n(9, NodeKind::Identifier { name: ident("x") }),
],
};
let n_extra = extra.len();
let (callee, args, mut call_node) = desugared_call(m, extra);
call_node.metadata.insert(
bock_types::checker::RECV_KIND_META_KEY.to_string(),
bock_air::Value::String("List".to_string()),
);
let (recv, got_method, rest) =
desugared_list_inplace_mutator(&call_node, &callee, &args).expect("should match");
assert_eq!(got_method, m);
assert!(matches!(&recv.kind, NodeKind::Identifier { name } if name.name == "nums"));
assert_eq!(rest.len(), n_extra);
}
}
#[test]
fn desugared_list_inplace_mutator_set_requires_explicit_list_stamp() {
let extra = vec![
n(7, NodeKind::Identifier { name: ident("i") }),
n(9, NodeKind::Identifier { name: ident("x") }),
];
let (callee, args, call_node) = desugared_call("set", extra);
assert!(
desugared_list_inplace_mutator(&call_node, &callee, &args).is_none(),
"unstamped `set` must not be claimed by the List mutator lowering"
);
let mut map_stamped = call_node.clone();
map_stamped.metadata.insert(
bock_types::checker::RECV_KIND_META_KEY.to_string(),
bock_air::Value::String("Map".to_string()),
);
assert!(
desugared_list_inplace_mutator(&map_stamped, &callee, &args).is_none(),
"Map-stamped `set` must not be claimed by the List mutator lowering"
);
let (callee_p, args_p, call_p) = desugared_call("pop", vec![]);
assert!(
desugared_list_inplace_mutator(&call_p, &callee_p, &args_p).is_some(),
"unstamped `pop` keeps the absent-stamp fall-through"
);
}
#[test]
fn desugared_list_inplace_mutator_rejects_user_stamp() {
for &m in INPLACE_LIST_MUTATORS {
let (callee, args, mut call_node) = desugared_call(m, vec![]);
call_node.metadata.insert(
bock_types::checker::RECV_KIND_META_KEY.to_string(),
bock_air::Value::String("User:Counter".to_string()),
);
assert!(
desugared_list_inplace_mutator(&call_node, &callee, &args).is_none(),
"{m} on a user record must not route to the List mutator lowering"
);
}
}
#[test]
fn desugared_list_method_accepts_explicit_list_stamp() {
let (callee, args, mut call_node) = desugared_call("len", vec![]);
call_node.metadata.insert(
bock_types::checker::RECV_KIND_META_KEY.to_string(),
bock_air::Value::String("List".to_string()),
);
assert!(
desugared_list_method(&call_node, &callee, &args).is_some(),
"a `recv_kind = \"List\"` len() must be recognised as the built-in"
);
}
#[test]
fn desugared_list_method_rejects_same_named_user_record_method() {
for &m in &["len", "is_empty", "contains", "count", "first"] {
let extra = if m == "contains" {
vec![n(7, NodeKind::Identifier { name: ident("x") })]
} else {
vec![]
};
let (callee, args, mut call_node) = desugared_call(m, extra);
call_node.metadata.insert(
bock_types::checker::RECV_KIND_META_KEY.to_string(),
bock_air::Value::String("User:Counter".to_string()),
);
assert!(
desugared_list_method(&call_node, &callee, &args).is_none(),
"{m} on a user record (recv_kind=User:Counter) must not route to the List built-in"
);
}
}
#[test]
fn desugared_list_functional_method_matches_closure_combinators() {
for &m in FUNCTIONAL_LIST_METHODS {
let extra = if m == "fold" {
vec![
n(
7,
NodeKind::Identifier {
name: ident("init"),
},
),
n(9, NodeKind::Identifier { name: ident("cb") }),
]
} else {
vec![n(7, NodeKind::Identifier { name: ident("cb") })]
};
let n_extra = extra.len();
let (callee, args, call_node) = desugared_call(m, extra);
let (recv, got_method, rest) =
desugared_list_functional_method(&call_node, &callee, &args).expect("should match");
assert_eq!(got_method, m);
assert!(matches!(&recv.kind, NodeKind::Identifier { name } if name.name == "nums"));
assert_eq!(rest.len(), n_extra);
}
}
#[test]
fn desugared_list_functional_method_rejects_read_only_and_other_stamps() {
for &m in &["len", "get", "concat", "join", "frobnicate"] {
let (callee, args, call_node) = desugared_call(m, vec![]);
assert!(
desugared_list_functional_method(&call_node, &callee, &args).is_none(),
"{m} must not be recognised as a functional List method"
);
}
let extra = vec![n(7, NodeKind::Identifier { name: ident("cb") })];
let (callee, args, mut call_node) = desugared_call("map", extra);
call_node.metadata.insert(
bock_types::checker::RECV_KIND_META_KEY.to_string(),
bock_air::Value::String("Set".to_string()),
);
assert!(
desugared_list_functional_method(&call_node, &callee, &args).is_none(),
"map on a Set receiver must not route to the List functional built-in"
);
}
#[test]
fn is_list_concat_reads_the_checker_stamp() {
let lhs = n(20, NodeKind::Identifier { name: ident("a") });
let rhs = n(21, NodeKind::Identifier { name: ident("b") });
let plain = n(1, NodeKind::Identifier { name: ident("x") });
assert!(
!is_list_concat(&plain, &lhs, &rhs),
"an unstamped node with non-literal operands is not list concat"
);
let mut stamped = n(2, NodeKind::Identifier { name: ident("x") });
stamped.metadata.insert(
bock_types::checker::LIST_CONCAT_META_KEY.to_string(),
bock_air::Value::Bool(true),
);
assert!(
is_list_concat(&stamped, &lhs, &rhs),
"the `Bool(true)` stamp marks list concat"
);
let list_lit = n(22, NodeKind::ListLiteral { elems: vec![] });
assert!(
is_list_concat(&plain, &lhs, &list_lit),
"a list-literal operand marks list concat syntactically"
);
}
#[test]
fn ordering_variant_recognises_only_the_three_variants() {
assert_eq!(ordering_variant("Less"), Some("Less"));
assert_eq!(ordering_variant("Equal"), Some("Equal"));
assert_eq!(ordering_variant("Greater"), Some("Greater"));
assert_eq!(ordering_variant("Some"), None);
assert_eq!(ordering_variant("less"), None);
assert_eq!(ordering_variant("Ordering"), None);
}
fn annotated_call(
method: &str,
tag: &str,
extra: Vec<AIRNode>,
) -> (AIRNode, AIRNode, Vec<AirArg>) {
let (callee, args, _) = desugared_call(method, extra);
let mut call = n(
100,
NodeKind::Call {
callee: Box::new(callee.clone()),
args: args.clone(),
type_args: vec![],
},
);
call.metadata.insert(
bock_types::checker::RECV_KIND_META_KEY.to_string(),
bock_air::Value::String(tag.to_string()),
);
(call, callee, args)
}
#[test]
fn primitive_recv_kind_reads_the_annotation() {
let (call, _, _) = annotated_call("compare", "Primitive:Int", vec![]);
assert_eq!(primitive_recv_kind(&call), Some("Int"));
let (call, _, _) = annotated_call("unwrap_or", "Optional", vec![]);
assert_eq!(primitive_recv_kind(&call), None);
let (callee, args, _) = desugared_call("compare", vec![]);
let bare = n(
101,
NodeKind::Call {
callee: Box::new(callee),
args,
type_args: vec![],
},
);
assert_eq!(primitive_recv_kind(&bare), None);
}
#[test]
fn primitive_bridge_call_matches_bridge_methods_on_primitive() {
for &m in PRIMITIVE_BRIDGE_METHODS {
let extra = if matches!(m, "compare" | "eq") {
vec![n(7, NodeKind::Identifier { name: ident("x") })]
} else {
vec![]
};
let n_extra = extra.len();
let (call, callee, args) = annotated_call(m, "Primitive:Int", extra);
let (recv, method, rest, prim) =
primitive_bridge_call(&call, &callee, &args).expect("should match");
assert_eq!(method, m);
assert_eq!(prim, "Int");
assert_eq!(rest.len(), n_extra);
assert!(matches!(&recv.kind, NodeKind::Identifier { name } if name.name == "nums"));
}
}
#[test]
fn primitive_bridge_call_rejects_non_primitive_and_unknown_methods() {
let (call, callee, args) = annotated_call("compare", "User:Point", vec![]);
assert!(primitive_bridge_call(&call, &callee, &args).is_none());
let (call, callee, args) = annotated_call("frobnicate", "Primitive:Int", vec![]);
assert!(primitive_bridge_call(&call, &callee, &args).is_none());
let (callee, args, _) = desugared_call("compare", vec![]);
let bare = n(
102,
NodeKind::Call {
callee: Box::new(callee.clone()),
args: args.clone(),
type_args: vec![],
},
);
assert!(primitive_bridge_call(&bare, &callee, &args).is_none());
}
#[test]
fn container_recv_kind_reads_optional_and_result() {
let (call, _, _) = annotated_call("unwrap_or", "Optional", vec![]);
assert_eq!(container_recv_kind(&call), Some("Optional"));
let (call, _, _) = annotated_call("unwrap_or", "Result", vec![]);
assert_eq!(container_recv_kind(&call), Some("Result"));
let (call, _, _) = annotated_call("unwrap_or", "List", vec![]);
assert_eq!(container_recv_kind(&call), None);
let (call, _, _) = annotated_call("compare", "Primitive:Int", vec![]);
assert_eq!(container_recv_kind(&call), None);
}
#[test]
fn desugared_optional_method_matches_optional_methods() {
for &m in OPTIONAL_METHODS {
let extra = if matches!(m, "unwrap_or" | "map" | "flat_map") {
vec![n(7, NodeKind::Identifier { name: ident("x") })]
} else {
vec![]
};
let n_extra = extra.len();
let (call, callee, args) = annotated_call(m, "Optional", extra);
let (recv, got, rest) =
desugared_optional_method(&call, &callee, &args).expect("should match");
assert_eq!(got, m);
assert_eq!(rest.len(), n_extra);
assert!(matches!(&recv.kind, NodeKind::Identifier { name } if name.name == "nums"));
let (call_r, callee_r, args_r) = annotated_call(m, "Result", vec![]);
assert!(desugared_optional_method(&call_r, &callee_r, &args_r).is_none());
}
}
#[test]
fn desugared_result_method_matches_result_methods() {
for &m in RESULT_METHODS {
let extra = if matches!(m, "unwrap_or" | "map" | "map_err") {
vec![n(7, NodeKind::Identifier { name: ident("x") })]
} else {
vec![]
};
let n_extra = extra.len();
let (call, callee, args) = annotated_call(m, "Result", extra);
let (recv, got, rest) =
desugared_result_method(&call, &callee, &args).expect("should match");
assert_eq!(got, m);
assert_eq!(rest.len(), n_extra);
assert!(matches!(&recv.kind, NodeKind::Identifier { name } if name.name == "nums"));
let (call_o, callee_o, args_o) = annotated_call(m, "Optional", vec![]);
assert!(desugared_result_method(&call_o, &callee_o, &args_o).is_none());
}
}
#[test]
fn container_methods_require_the_annotation() {
let (callee, args, _) = desugared_call("unwrap_or", vec![]);
let bare = n(
103,
NodeKind::Call {
callee: Box::new(callee.clone()),
args: args.clone(),
type_args: vec![],
},
);
assert!(desugared_optional_method(&bare, &callee, &args).is_none());
assert!(desugared_result_method(&bare, &callee, &args).is_none());
let (call, callee, args) = annotated_call("frobnicate", "Optional", vec![]);
assert!(desugared_optional_method(&call, &callee, &args).is_none());
}
#[test]
fn container_recv_kind_reads_map_and_set() {
let (call, _, _) = annotated_call("get", "Map", vec![]);
assert_eq!(container_recv_kind(&call), Some("Map"));
let (call, _, _) = annotated_call("add", "Set", vec![]);
assert_eq!(container_recv_kind(&call), Some("Set"));
}
#[test]
fn desugared_map_method_matches_map_methods() {
for &m in MAP_METHODS {
let (call, callee, args) = annotated_call(m, "Map", vec![]);
let (recv, got, _rest) =
desugared_map_method(&call, &callee, &args).expect("should match");
assert_eq!(got, m);
assert!(matches!(&recv.kind, NodeKind::Identifier { name } if name.name == "nums"));
let (call_s, callee_s, args_s) = annotated_call(m, "Set", vec![]);
if SET_METHODS.contains(&m) {
assert!(desugared_map_method(&call_s, &callee_s, &args_s).is_none());
}
}
let (call, callee, args) = annotated_call("contains", "Map", vec![]);
assert!(desugared_map_method(&call, &callee, &args).is_none());
}
#[test]
fn desugared_set_method_matches_set_methods() {
for &m in SET_METHODS {
let (call, callee, args) = annotated_call(m, "Set", vec![]);
let (recv, got, _rest) =
desugared_set_method(&call, &callee, &args).expect("should match");
assert_eq!(got, m);
assert!(matches!(&recv.kind, NodeKind::Identifier { name } if name.name == "nums"));
let (call_m, callee_m, args_m) = annotated_call(m, "Map", vec![]);
if MAP_METHODS.contains(&m) {
assert!(desugared_set_method(&call_m, &callee_m, &args_m).is_none());
}
}
}
#[test]
fn desugared_string_method_matches_string_methods_on_primitive_string() {
for &m in STRING_METHODS {
let extra = vec![n(7, NodeKind::Identifier { name: ident("x") })];
let (call, callee, args) = annotated_call(m, "Primitive:String", extra);
let (recv, got, rest) =
desugared_string_method(&call, &callee, &args).expect("should match");
assert_eq!(got, m);
assert_eq!(rest.len(), 1);
assert!(matches!(&recv.kind, NodeKind::Identifier { name } if name.name == "nums"));
let (call_i, callee_i, args_i) = annotated_call(m, "Primitive:Int", vec![]);
assert!(desugared_string_method(&call_i, &callee_i, &args_i).is_none());
}
}
#[test]
fn desugared_string_method_rejects_unknown_methods_and_missing_annotation() {
let (call, callee, args) = annotated_call("frobnicate", "Primitive:String", vec![]);
assert!(desugared_string_method(&call, &callee, &args).is_none());
let (callee, args, _) = desugared_call(
"contains",
vec![n(7, NodeKind::Identifier { name: ident("x") })],
);
let bare = n(
105,
NodeKind::Call {
callee: Box::new(callee.clone()),
args: args.clone(),
type_args: vec![],
},
);
assert!(desugared_string_method(&bare, &callee, &args).is_none());
}
#[test]
fn map_set_methods_require_the_annotation() {
let (callee, args, _) = desugared_call("get", vec![]);
let bare = n(
104,
NodeKind::Call {
callee: Box::new(callee.clone()),
args: args.clone(),
type_args: vec![],
},
);
assert!(desugared_map_method(&bare, &callee, &args).is_none());
assert!(desugared_set_method(&bare, &callee, &args).is_none());
}
#[test]
fn param_binds_self_detects_self_param() {
let self_p = n(
1,
NodeKind::Param {
pattern: Box::new(n(
2,
NodeKind::BindPat {
name: ident("self"),
is_mut: false,
},
)),
ty: None,
default: None,
},
);
assert_eq!(param_binds_self(&self_p), Some(false));
let other = n(
3,
NodeKind::Param {
pattern: Box::new(n(
4,
NodeKind::BindPat {
name: ident("x"),
is_mut: false,
},
)),
ty: None,
default: None,
},
);
assert_eq!(param_binds_self(&other), None);
}
#[test]
fn loop_needs_break_label_when_match_arm_breaks() {
let match_node = n(
1,
NodeKind::Match {
scrutinee: Box::new(n(2, NodeKind::Identifier { name: ident("i") })),
arms: vec![match_arm(3, n(5, NodeKind::Break { value: None }))],
},
);
let body = n(
6,
NodeKind::Block {
stmts: vec![match_node],
tail: None,
},
);
assert!(loop_needs_break_label(&body));
let value_match = n(
10,
NodeKind::Match {
scrutinee: Box::new(n(11, NodeKind::Identifier { name: ident("i") })),
arms: vec![match_arm(
12,
n(
14,
NodeKind::Literal {
lit: bock_ast::Literal::Int("0".into()),
},
),
)],
},
);
let body2 = n(
15,
NodeKind::Block {
stmts: vec![value_match],
tail: None,
},
);
assert!(!loop_needs_break_label(&body2));
}
fn enum_variant(name: &str, payload: EnumVariantPayload) -> AIRNode {
n(
0,
NodeKind::EnumVariant {
name: ident(name),
payload,
},
)
}
fn record_field(name: &str) -> bock_ast::RecordDeclField {
bock_ast::RecordDeclField {
id: 0,
span: dummy_span(),
name: ident(name),
ty: bock_ast::TypeExpr::Named {
id: 0,
span: dummy_span(),
path: bock_ast::TypePath {
segments: vec![ident("Int")],
span: dummy_span(),
},
args: vec![],
},
default: None,
}
}
fn enum_decl(name: &str, variants: Vec<AIRNode>) -> AIRNode {
n(
0,
NodeKind::EnumDecl {
annotations: vec![],
visibility: bock_ast::Visibility::Public,
name: ident(name),
generic_params: vec![],
variants,
},
)
}
fn variant_path(name: &str) -> bock_ast::TypePath {
bock_ast::TypePath {
segments: vec![ident(name)],
span: dummy_span(),
}
}
#[test]
fn collect_enum_variants_records_all_payload_kinds() {
let shape = enum_decl(
"Shape",
vec![
enum_variant(
"Circle",
EnumVariantPayload::Struct(vec![record_field("radius")]),
),
enum_variant(
"Rect",
EnumVariantPayload::Tuple(vec![
n(1, NodeKind::Placeholder),
n(2, NodeKind::Placeholder),
]),
),
enum_variant("Empty", EnumVariantPayload::Unit),
],
);
let m = module_named("main", &[], vec![shape]);
let p = std::path::Path::new("x.bock");
let reg = collect_enum_variants(&[(&m, p)]);
let circle = reg.get("Circle").expect("Circle registered");
assert_eq!(circle.enum_name, "Shape");
assert_eq!(
circle.payload,
VariantPayloadKind::Struct(vec!["radius".to_string()])
);
let rect = reg.get("Rect").expect("Rect registered");
assert_eq!(rect.enum_name, "Shape");
assert_eq!(rect.payload, VariantPayloadKind::Tuple(2));
let empty = reg.get("Empty").expect("Empty registered");
assert_eq!(empty.enum_name, "Shape");
assert_eq!(empty.payload, VariantPayloadKind::Unit);
}
#[test]
fn collect_enum_variants_pre_seeds_optional_and_result() {
let reg = collect_enum_variants(&[]);
assert_eq!(
reg.get("Some").map(|i| i.enum_name.as_str()),
Some("Optional")
);
assert_eq!(
reg.get("Some").map(|i| &i.payload),
Some(&VariantPayloadKind::Tuple(1))
);
assert_eq!(
reg.get("None").map(|i| &i.payload),
Some(&VariantPayloadKind::Unit)
);
assert_eq!(reg.get("Ok").map(|i| i.enum_name.as_str()), Some("Result"));
assert_eq!(reg.get("Err").map(|i| i.enum_name.as_str()), Some("Result"));
}
#[test]
fn collect_enum_variants_spans_multiple_modules() {
let color = enum_decl("Color", vec![enum_variant("Red", EnumVariantPayload::Unit)]);
let lib = module_named("lib", &[], vec![color]);
let main_m = module_named("main", &["lib"], vec![fn_decl("main")]);
let p = std::path::Path::new("x.bock");
let reg = collect_enum_variants(&[(&lib, p), (&main_m, p)]);
assert_eq!(reg.get("Red").map(|i| i.enum_name.as_str()), Some("Color"));
}
fn fn_decl_with_body(name: &str, stmts: Vec<AIRNode>) -> AIRNode {
let body = AIRNode::new(900, dummy_span(), NodeKind::Block { stmts, tail: None });
AIRNode::new(
0,
dummy_span(),
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Public,
is_async: false,
name: ident(name),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
)
}
#[test]
fn implicit_esm_imports_glob_imported_enum_variant_by_bare_name() {
let category = enum_decl(
"Category",
vec![
enum_variant("Electronics", EnumVariantPayload::Unit),
enum_variant("Clothing", EnumVariantPayload::Unit),
],
);
let models = module_named("models", &[], vec![category]);
let use_electronics = AIRNode::new(
901,
dummy_span(),
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(AIRNode::new(
902,
dummy_span(),
NodeKind::BindPat {
name: ident("_"),
is_mut: false,
},
)),
ty: None,
value: Box::new(identifier(903, "Electronics")),
},
);
let main_m = module_named(
"main",
&["models"],
vec![fn_decl_with_body("use_it", vec![use_electronics])],
);
let p = std::path::Path::new("x.bock");
let public_symbols = collect_public_symbols_for_esm(&[(&models, p), (&main_m, p)]);
let imports = implicit_esm_imports_for(&main_m, &public_symbols, "main");
let variant_import = imports
.iter()
.find(|i| i.name == "Category_Electronics")
.expect(
"glob-imported bare variant `Electronics` must import as `Category_Electronics`",
);
assert_eq!(variant_import.module_path, "models");
assert_eq!(variant_import.kind, EsmDeclKind::EnumVariant);
assert!(
!imports.iter().any(|i| i.name == "Category_Clothing"),
"unreferenced variant `Clothing` must not be imported; got: {imports:?}"
);
}
#[test]
fn registered_variant_resolves_last_path_segment() {
let shape = enum_decl(
"Shape",
vec![enum_variant("Empty", EnumVariantPayload::Unit)],
);
let m = module_named("main", &[], vec![shape]);
let p = std::path::Path::new("x.bock");
let reg = collect_enum_variants(&[(&m, p)]);
assert_eq!(
registered_variant(®, &variant_path("Empty")).map(|i| i.enum_name.as_str()),
Some("Shape")
);
assert!(registered_variant(®, &variant_path("Nope")).is_none());
}
fn generic_record_decl(name: &str, params: &[&str]) -> AIRNode {
n(
0,
NodeKind::RecordDecl {
annotations: vec![],
visibility: bock_ast::Visibility::Public,
name: ident(name),
generic_params: params
.iter()
.map(|p| bock_ast::GenericParam {
id: 0,
span: dummy_span(),
name: ident(p),
bounds: vec![],
})
.collect(),
fields: vec![record_field("value")],
},
)
}
#[test]
fn collect_generic_decls_records_params_and_spans_modules() {
let boxed = generic_record_decl("Box", &["T"]);
let pair = generic_record_decl("Pair", &["A", "B"]);
let plain = generic_record_decl("Plain", &[]);
let lib = module_named("lib", &[], vec![pair]);
let main_m = module_named("main", &["lib"], vec![boxed, plain]);
let p = std::path::Path::new("x.bock");
let reg = collect_generic_decls(&[(&lib, p), (&main_m, p)]);
let box_params = reg.get("Box").expect("Box registered");
assert_eq!(box_params.len(), 1);
assert_eq!(box_params[0].name.name, "T");
let pair_params = reg.get("Pair").expect("Pair registered");
assert_eq!(pair_params.len(), 2);
assert_eq!(pair_params[0].name.name, "A");
assert_eq!(pair_params[1].name.name, "B");
assert_eq!(reg.get("Plain").map(Vec::len), Some(0));
assert!(!reg.contains_key("Nope"));
}
fn trait_method(name: &str, default_body: bool, self_operand: bool) -> AIRNode {
let tail = if default_body {
Some(Box::new(n(
50,
NodeKind::Literal {
lit: bock_ast::Literal::Unit,
},
)))
} else {
None
};
let body = n(
40,
NodeKind::Block {
stmts: vec![],
tail,
},
);
let mut params = vec![n(
41,
NodeKind::Param {
pattern: Box::new(n(
42,
NodeKind::BindPat {
name: ident("self"),
is_mut: false,
},
)),
ty: None,
default: None,
},
)];
if self_operand {
params.push(n(
43,
NodeKind::Param {
pattern: Box::new(n(
44,
NodeKind::BindPat {
name: ident("other"),
is_mut: false,
},
)),
ty: Some(Box::new(n(45, NodeKind::TypeSelf))),
default: None,
},
));
}
n(
10,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident(name),
generic_params: vec![],
params,
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
)
}
fn trait_decl(name: &str, methods: Vec<AIRNode>) -> AIRNode {
n(
5,
NodeKind::TraitDecl {
annotations: vec![],
visibility: Visibility::Public,
is_platform: false,
name: ident(name),
generic_params: vec![],
associated_types: vec![],
methods,
},
)
}
#[test]
fn is_default_method_uses_empty_block_heuristic() {
assert!(is_default_method(&trait_method("dflt", true, false)));
assert!(!is_default_method(&trait_method("req", false, false)));
}
#[test]
fn collect_trait_decls_records_methods_and_spans_modules() {
let eq = trait_decl(
"Eq",
vec![
trait_method("equals", false, true),
trait_method("not_equals", true, true),
],
);
let other = trait_decl("Show", vec![trait_method("show", false, false)]);
let lib = module_named("lib", &[], vec![other]);
let main_m = module_named("main", &["lib"], vec![eq]);
let p = std::path::Path::new("x.bock");
let reg = collect_trait_decls(&[(&lib, p), (&main_m, p)]);
let eq_info = reg.get("Eq").expect("Eq registered");
assert_eq!(eq_info.methods.len(), 2);
assert!(reg.contains_key("Show"));
}
#[test]
fn inherited_default_methods_excludes_overridden_and_required() {
let eq = trait_decl(
"Eq",
vec![
trait_method("equals", false, true),
trait_method("not_equals", true, true),
],
);
let m = module_named("main", &[], vec![eq]);
let p = std::path::Path::new("x.bock");
let reg = collect_trait_decls(&[(&m, p)]);
let trait_path = variant_path("Eq");
let impl_methods = vec![fn_decl("equals")];
let inherited = inherited_default_methods(®, &trait_path, &impl_methods);
assert_eq!(inherited.len(), 1);
assert_eq!(fn_decl_name(&inherited[0]), Some("not_equals"));
let impl_methods = vec![fn_decl("equals"), fn_decl("not_equals")];
assert!(inherited_default_methods(®, &trait_path, &impl_methods).is_empty());
let inherited = inherited_default_methods(®, &trait_path, &[]);
assert_eq!(inherited.len(), 1);
assert_eq!(fn_decl_name(&inherited[0]), Some("not_equals"));
}
#[test]
fn trait_uses_self_operand_detects_self_typed_params() {
let with_self = TraitDeclInfo {
generic_params: vec![],
methods: vec![trait_method("equals", false, true)],
};
assert!(trait_uses_self_operand(&with_self));
let without_self = TraitDeclInfo {
generic_params: vec![],
methods: vec![trait_method("show", false, false)],
};
assert!(!trait_uses_self_operand(&without_self));
}
#[test]
fn collect_exported_type_names_records_only_public_types() {
let pub_rec = generic_record_decl("Key", &[]); let priv_rec = n(
70,
NodeKind::RecordDecl {
annotations: vec![],
visibility: Visibility::Private,
name: ident("Hidden"),
generic_params: vec![],
fields: vec![],
},
);
let m = module_named("main", &[], vec![pub_rec, priv_rec]);
let p = std::path::Path::new("x.bock");
let names = collect_exported_type_names(&[(&m, p)]);
assert!(names.contains("Key"));
assert!(!names.contains("Hidden"));
}
fn return_int(id: u32) -> AIRNode {
n(
id,
NodeKind::Return {
value: Some(Box::new(int_lit(id + 1))),
},
)
}
#[test]
fn value_cf_diverges_detects_if_with_return_branch() {
let node = if_node(
1,
block_with_tail(2, int_lit(3)),
Some(block_with_tail(4, return_int(5))),
);
assert!(value_cf_diverges(&node));
}
#[test]
fn value_cf_diverges_skips_plain_value_if() {
let node = if_node(
1,
block_with_tail(2, int_lit(3)),
Some(block_with_tail(4, int_lit(5))),
);
assert!(!value_cf_diverges(&node));
}
#[test]
fn value_cf_diverges_detects_nested_else_if_chain() {
let inner = if_node(
10,
block_with_tail(11, int_lit(12)),
Some(block_with_tail(13, return_int(14))),
);
let outer = if_node(1, block_with_tail(2, int_lit(3)), Some(inner));
assert!(value_cf_diverges(&outer));
}
#[test]
fn value_cf_diverges_hoists_value_loop_only() {
let value_loop = n(
1,
NodeKind::Loop {
body: Box::new(block_with_tail(
2,
n(
3,
NodeKind::Break {
value: Some(Box::new(int_lit(4))),
},
),
)),
},
);
assert!(value_cf_diverges(&value_loop));
let unit_loop = n(
10,
NodeKind::Loop {
body: Box::new(block_with_tail(11, n(12, NodeKind::Break { value: None }))),
},
);
assert!(!value_cf_diverges(&unit_loop));
}
#[test]
fn value_cf_diverges_detects_match_with_return_arm() {
let arms = vec![
match_arm(10, int_lit(12)),
match_arm(20, n(22, NodeKind::Return { value: None })),
];
let m = n(
1,
NodeKind::Match {
scrutinee: Box::new(n(2, NodeKind::Placeholder)),
arms,
},
);
assert!(value_cf_diverges(&m));
}
fn hoisted_let_block(value: AIRNode) -> AIRNode {
let let_pat = n(
900,
NodeKind::BindPat {
name: ident("x"),
is_mut: false,
},
);
let let_binding = n(
901,
NodeKind::LetBinding {
is_mut: false,
pattern: Box::new(let_pat),
ty: None,
value: Box::new(value),
},
);
let body = n(
902,
NodeKind::Block {
stmts: vec![let_binding],
tail: None,
},
);
let fn_decl = n(
903,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Private,
is_async: false,
name: ident("f"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
);
module_named("main", &[], vec![fn_decl])
}
fn fn_body_stmts(module: &AIRNode) -> &[AIRNode] {
let NodeKind::Module { items, .. } = &module.kind else {
panic!("module");
};
let NodeKind::FnDecl { body, .. } = &items[0].kind else {
panic!("fn");
};
let NodeKind::Block { stmts, .. } = &body.kind else {
panic!("body block");
};
stmts
}
fn find_let_x(stmts: &[AIRNode]) -> &AIRNode {
stmts
.iter()
.find(|s| {
matches!(&s.kind, NodeKind::LetBinding { pattern, .. }
if matches!(&pattern.kind, NodeKind::BindPat { name, .. } if name.name == "x"))
})
.expect("let x binding")
}
#[test]
fn hoist_rewrites_diverging_let_into_prelude_and_temp_read() {
let value = if_node(
1,
block_with_tail(2, int_lit(3)),
Some(block_with_tail(4, return_int(5))),
);
let module = hoist_value_cf(hoisted_let_block(value));
let stmts = fn_body_stmts(&module);
assert_eq!(stmts.len(), 3, "decl + CF-stmt + let; got {}", stmts.len());
assert!(
matches!(&stmts[0].kind, NodeKind::LetBinding { is_mut: true, .. }),
"first stmt must be the mut temp decl, got {:?}",
stmts[0].kind
);
assert_eq!(
stmts[0].metadata.get(DECL_ONLY_META),
Some(&bock_air::stubs::Value::Bool(true)),
"temp decl must carry the declare-only marker"
);
let NodeKind::If {
then_block,
else_block,
..
} = &stmts[1].kind
else {
panic!("expected relocated If, got {:?}", stmts[1].kind);
};
let NodeKind::Block {
stmts: then_stmts,
tail: then_tail,
} = &then_block.kind
else {
panic!("then block");
};
let then_last = then_tail
.as_deref()
.or_else(|| then_stmts.last())
.map(|t| &t.kind);
assert!(
matches!(then_last, Some(NodeKind::Assign { .. })),
"value arm must end in an Assign, got {then_last:?}"
);
let NodeKind::Block {
stmts: else_stmts,
tail: else_tail,
} = &else_block.as_ref().unwrap().kind
else {
panic!("else block");
};
let else_last = else_tail
.as_deref()
.or_else(|| else_stmts.last())
.map(|t| &t.kind);
assert!(
matches!(else_last, Some(NodeKind::Return { .. })),
"diverging arm must keep its return, got {else_last:?}"
);
let NodeKind::LetBinding { value, .. } = &find_let_x(stmts).kind else {
panic!("let x");
};
assert!(
matches!(&value.kind, NodeKind::Identifier { name } if name.name.starts_with("__bock_cf_")),
"let value must read the temp identifier, got {:?}",
value.kind
);
}
#[test]
fn hoist_leaves_plain_value_let_untouched() {
let value = if_node(
1,
block_with_tail(2, int_lit(3)),
Some(block_with_tail(4, int_lit(5))),
);
let module = hoist_value_cf(hoisted_let_block(value));
let stmts = fn_body_stmts(&module);
assert_eq!(stmts.len(), 1, "no prelude for a plain value if");
let NodeKind::LetBinding { value, .. } = &stmts[0].kind else {
panic!("let");
};
assert!(
matches!(&value.kind, NodeKind::If { .. }),
"plain value if must stay the let's If value, got {:?}",
value.kind
);
}
#[test]
fn hoist_rewrites_loop_break_value() {
let loop_value = n(
1,
NodeKind::Loop {
body: Box::new(n(
2,
NodeKind::Block {
stmts: vec![n(
3,
NodeKind::Break {
value: Some(Box::new(int_lit(4))),
},
)],
tail: None,
},
)),
},
);
let module = hoist_value_cf(hoisted_let_block(loop_value));
let stmts = fn_body_stmts(&module);
assert_eq!(stmts.len(), 3);
assert!(matches!(&stmts[1].kind, NodeKind::Loop { .. }));
let mut found_bare_break = false;
struct BreakFinder<'a>(&'a mut bool);
impl bock_air::visitor::Visitor for BreakFinder<'_> {
fn visit_node(&mut self, node: &AIRNode) {
if matches!(&node.kind, NodeKind::Break { value: None }) {
*self.0 = true;
}
bock_air::visitor::walk_node(self, node);
}
}
use bock_air::visitor::Visitor;
BreakFinder(&mut found_bare_break).visit_node(&stmts[1]);
assert!(
found_bare_break,
"break value must be hoisted into an Assign, leaving a bare break"
);
}
}