use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicU32, Ordering};
use bock_ast::{
Annotation, AssignOp, BinOp, GenericParam, Ident, ImportItems, Literal, ModulePath,
PropertyBinding, RecordDeclField, TypeConstraint, TypePath, UnaryOp, Visibility,
};
use bock_errors::Span;
use crate::stubs::{
Capability, ContextBlock, EffectRef, OwnershipInfo, TargetInfo, TypeInfo, Value,
};
pub type NodeId = u32;
#[derive(Debug, Default)]
pub struct NodeIdGen {
counter: AtomicU32,
}
impl NodeIdGen {
#[must_use]
pub fn new() -> Self {
Self {
counter: AtomicU32::new(0),
}
}
#[must_use]
pub fn next(&self) -> NodeId {
self.counter.fetch_add(1, Ordering::SeqCst)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct AIRNode {
pub id: NodeId,
pub span: Span,
pub kind: NodeKind,
pub type_info: Option<TypeInfo>,
pub ownership: Option<OwnershipInfo>,
pub effects: HashSet<EffectRef>,
pub capabilities: HashSet<Capability>,
pub context: Option<ContextBlock>,
pub target: Option<TargetInfo>,
pub metadata: HashMap<String, Value>,
}
impl AIRNode {
#[must_use]
pub fn new(id: NodeId, span: Span, kind: NodeKind) -> Self {
Self {
id,
span,
kind,
type_info: None,
ownership: None,
effects: HashSet::new(),
capabilities: HashSet::new(),
context: None,
target: None,
metadata: HashMap::new(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct AirArg {
pub label: Option<Ident>,
pub value: AIRNode,
}
#[derive(Debug, Clone, PartialEq)]
pub struct AirRecordField {
pub name: Ident,
pub value: Option<Box<AIRNode>>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct AirRecordPatternField {
pub name: Ident,
pub pattern: Option<Box<AIRNode>>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct AirMapEntry {
pub key: AIRNode,
pub value: AIRNode,
}
#[derive(Debug, Clone, PartialEq)]
pub struct AirHandlerPair {
pub effect: TypePath,
pub handler: Box<AIRNode>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum AirInterpolationPart {
Literal(String),
Expr(Box<AIRNode>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResultVariant {
Ok,
Err,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum NodeKind {
Module {
path: Option<ModulePath>,
annotations: Vec<Annotation>,
imports: Vec<AIRNode>,
items: Vec<AIRNode>,
},
ImportDecl {
path: ModulePath,
items: ImportItems,
},
FnDecl {
annotations: Vec<Annotation>,
visibility: Visibility,
is_async: bool,
name: Ident,
generic_params: Vec<GenericParam>,
params: Vec<AIRNode>,
return_type: Option<Box<AIRNode>>,
effect_clause: Vec<TypePath>,
where_clause: Vec<TypeConstraint>,
body: Box<AIRNode>,
},
RecordDecl {
annotations: Vec<Annotation>,
visibility: Visibility,
name: Ident,
generic_params: Vec<GenericParam>,
fields: Vec<RecordDeclField>,
},
EnumDecl {
annotations: Vec<Annotation>,
visibility: Visibility,
name: Ident,
generic_params: Vec<GenericParam>,
variants: Vec<AIRNode>,
},
EnumVariant {
name: Ident,
payload: EnumVariantPayload,
},
ClassDecl {
annotations: Vec<Annotation>,
visibility: Visibility,
name: Ident,
generic_params: Vec<GenericParam>,
base: Option<TypePath>,
traits: Vec<TypePath>,
fields: Vec<RecordDeclField>,
methods: Vec<AIRNode>,
},
TraitDecl {
annotations: Vec<Annotation>,
visibility: Visibility,
is_platform: bool,
name: Ident,
generic_params: Vec<GenericParam>,
associated_types: Vec<bock_ast::AssociatedType>,
methods: Vec<AIRNode>,
},
ImplBlock {
annotations: Vec<Annotation>,
generic_params: Vec<GenericParam>,
trait_path: Option<TypePath>,
target: Box<AIRNode>,
where_clause: Vec<TypeConstraint>,
methods: Vec<AIRNode>,
},
EffectDecl {
annotations: Vec<Annotation>,
visibility: Visibility,
name: Ident,
generic_params: Vec<GenericParam>,
components: Vec<TypePath>,
operations: Vec<AIRNode>,
},
TypeAlias {
annotations: Vec<Annotation>,
visibility: Visibility,
name: Ident,
generic_params: Vec<GenericParam>,
ty: Box<AIRNode>,
where_clause: Vec<TypeConstraint>,
},
ConstDecl {
annotations: Vec<Annotation>,
visibility: Visibility,
name: Ident,
ty: Box<AIRNode>,
value: Box<AIRNode>,
},
ModuleHandle {
effect: TypePath,
handler: Box<AIRNode>,
},
PropertyTest {
name: String,
bindings: Vec<PropertyBinding>,
body: Box<AIRNode>,
},
Param {
pattern: Box<AIRNode>,
ty: Option<Box<AIRNode>>,
default: Option<Box<AIRNode>>,
},
TypeNamed {
path: TypePath,
args: Vec<AIRNode>,
},
TypeTuple {
elems: Vec<AIRNode>,
},
TypeFunction {
params: Vec<AIRNode>,
ret: Box<AIRNode>,
effects: Vec<TypePath>,
},
TypeOptional {
inner: Box<AIRNode>,
},
TypeSelf,
Literal { lit: Literal },
Identifier { name: Ident },
BinaryOp {
op: BinOp,
left: Box<AIRNode>,
right: Box<AIRNode>,
},
UnaryOp { op: UnaryOp, operand: Box<AIRNode> },
Assign {
op: AssignOp,
target: Box<AIRNode>,
value: Box<AIRNode>,
},
Call {
callee: Box<AIRNode>,
args: Vec<AirArg>,
type_args: Vec<AIRNode>,
},
MethodCall {
receiver: Box<AIRNode>,
method: Ident,
type_args: Vec<AIRNode>,
args: Vec<AirArg>,
},
FieldAccess { object: Box<AIRNode>, field: Ident },
Index {
object: Box<AIRNode>,
index: Box<AIRNode>,
},
Propagate { expr: Box<AIRNode> },
Lambda {
params: Vec<AIRNode>,
body: Box<AIRNode>,
},
Pipe {
left: Box<AIRNode>,
right: Box<AIRNode>,
},
Compose {
left: Box<AIRNode>,
right: Box<AIRNode>,
},
Await { expr: Box<AIRNode> },
Range {
lo: Box<AIRNode>,
hi: Box<AIRNode>,
inclusive: bool,
},
RecordConstruct {
path: TypePath,
fields: Vec<AirRecordField>,
spread: Option<Box<AIRNode>>,
},
ListLiteral { elems: Vec<AIRNode> },
MapLiteral { entries: Vec<AirMapEntry> },
SetLiteral { elems: Vec<AIRNode> },
TupleLiteral { elems: Vec<AIRNode> },
Interpolation { parts: Vec<AirInterpolationPart> },
Placeholder,
Unreachable,
ResultConstruct {
variant: ResultVariant,
value: Option<Box<AIRNode>>,
},
If {
let_pattern: Option<Box<AIRNode>>,
condition: Box<AIRNode>,
then_block: Box<AIRNode>,
else_block: Option<Box<AIRNode>>,
},
Guard {
let_pattern: Option<Box<AIRNode>>,
condition: Box<AIRNode>,
else_block: Box<AIRNode>,
},
Match {
scrutinee: Box<AIRNode>,
arms: Vec<AIRNode>,
},
MatchArm {
pattern: Box<AIRNode>,
guard: Option<Box<AIRNode>>,
body: Box<AIRNode>,
},
For {
pattern: Box<AIRNode>,
iterable: Box<AIRNode>,
body: Box<AIRNode>,
},
While {
condition: Box<AIRNode>,
body: Box<AIRNode>,
},
Loop { body: Box<AIRNode> },
Block {
stmts: Vec<AIRNode>,
tail: Option<Box<AIRNode>>,
},
Return { value: Option<Box<AIRNode>> },
Break { value: Option<Box<AIRNode>> },
Continue,
LetBinding {
is_mut: bool,
pattern: Box<AIRNode>,
ty: Option<Box<AIRNode>>,
value: Box<AIRNode>,
},
Move { expr: Box<AIRNode> },
Borrow { expr: Box<AIRNode> },
MutableBorrow { expr: Box<AIRNode> },
EffectOp {
effect: TypePath,
operation: Ident,
args: Vec<AirArg>,
},
HandlingBlock {
handlers: Vec<AirHandlerPair>,
body: Box<AIRNode>,
},
EffectRef { path: TypePath },
WildcardPat,
BindPat { name: Ident, is_mut: bool },
LiteralPat { lit: Literal },
ConstructorPat {
path: TypePath,
fields: Vec<AIRNode>,
},
RecordPat {
path: TypePath,
fields: Vec<AirRecordPatternField>,
rest: bool,
},
TuplePat { elems: Vec<AIRNode> },
ListPat {
elems: Vec<AIRNode>,
rest: Option<Box<AIRNode>>,
},
OrPat { alternatives: Vec<AIRNode> },
GuardPat {
pattern: Box<AIRNode>,
guard: Box<AIRNode>,
},
RangePat {
lo: Box<AIRNode>,
hi: Box<AIRNode>,
inclusive: bool,
},
RestPat,
Error,
}
#[derive(Debug, Clone, PartialEq)]
pub enum EnumVariantPayload {
Unit,
Struct(Vec<RecordDeclField>),
Tuple(Vec<AIRNode>),
}
#[cfg(test)]
mod tests {
use super::*;
use bock_errors::FileId;
fn dummy_span() -> Span {
Span {
file: FileId(0),
start: 0,
end: 0,
}
}
fn dummy_ident(name: &str) -> Ident {
Ident {
name: name.to_string(),
span: dummy_span(),
}
}
fn make_node(id: NodeId, kind: NodeKind) -> AIRNode {
AIRNode::new(id, dummy_span(), kind)
}
#[test]
fn node_id_gen_monotonic() {
let gen = NodeIdGen::new();
let a = gen.next();
let b = gen.next();
let c = gen.next();
assert_eq!(a, 0);
assert_eq!(b, 1);
assert_eq!(c, 2);
}
#[test]
fn node_id_gen_thread_safe() {
use std::sync::Arc;
use std::thread;
let gen = Arc::new(NodeIdGen::new());
let handles: Vec<_> = (0..4)
.map(|_| {
let g = Arc::clone(&gen);
thread::spawn(move || g.next())
})
.collect();
let mut ids: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
ids.sort();
assert_eq!(ids, vec![0, 1, 2, 3]);
}
#[test]
fn air_node_new_has_empty_slots() {
let node = make_node(42, NodeKind::Continue);
assert_eq!(node.id, 42);
assert!(node.type_info.is_none());
assert!(node.ownership.is_none());
assert!(node.effects.is_empty());
assert!(node.capabilities.is_empty());
assert!(node.context.is_none());
assert!(node.target.is_none());
assert!(node.metadata.is_empty());
}
#[test]
fn air_node_debug_contains_kind() {
let node = make_node(0, NodeKind::Unreachable);
let s = format!("{node:?}");
assert!(s.contains("Unreachable"));
}
#[test]
fn module_node() {
let n = make_node(
0,
NodeKind::Module {
path: None,
annotations: vec![],
imports: vec![],
items: vec![],
},
);
assert!(matches!(n.kind, NodeKind::Module { .. }));
}
#[test]
fn fn_decl_node() {
let body = make_node(
1,
NodeKind::Block {
stmts: vec![],
tail: None,
},
);
let n = make_node(
0,
NodeKind::FnDecl {
annotations: vec![],
visibility: Visibility::Public,
is_async: false,
name: dummy_ident("foo"),
generic_params: vec![],
params: vec![],
return_type: None,
effect_clause: vec![],
where_clause: vec![],
body: Box::new(body),
},
);
assert!(matches!(n.kind, NodeKind::FnDecl { .. }));
}
#[test]
fn binary_op_node() {
let left = make_node(
1,
NodeKind::Literal {
lit: Literal::Int("1".into()),
},
);
let right = make_node(
2,
NodeKind::Literal {
lit: Literal::Int("2".into()),
},
);
let n = make_node(
0,
NodeKind::BinaryOp {
op: BinOp::Add,
left: Box::new(left),
right: Box::new(right),
},
);
assert!(matches!(n.kind, NodeKind::BinaryOp { op: BinOp::Add, .. }));
}
#[test]
fn pattern_nodes() {
let wildcard = make_node(0, NodeKind::WildcardPat);
let bind = make_node(
1,
NodeKind::BindPat {
name: dummy_ident("x"),
is_mut: false,
},
);
let lit = make_node(
2,
NodeKind::LiteralPat {
lit: Literal::Bool(true),
},
);
assert!(matches!(wildcard.kind, NodeKind::WildcardPat));
assert!(matches!(bind.kind, NodeKind::BindPat { .. }));
assert!(matches!(lit.kind, NodeKind::LiteralPat { .. }));
}
#[test]
fn control_flow_nodes() {
let body = Box::new(make_node(
1,
NodeKind::Block {
stmts: vec![],
tail: None,
},
));
let cond = Box::new(make_node(
2,
NodeKind::Literal {
lit: Literal::Bool(true),
},
));
let while_node = make_node(
0,
NodeKind::While {
condition: cond.clone(),
body: body.clone(),
},
);
let loop_node = make_node(3, NodeKind::Loop { body: body.clone() });
let return_node = make_node(4, NodeKind::Return { value: None });
let break_node = make_node(5, NodeKind::Break { value: None });
let continue_node = make_node(6, NodeKind::Continue);
assert!(matches!(while_node.kind, NodeKind::While { .. }));
assert!(matches!(loop_node.kind, NodeKind::Loop { .. }));
assert!(matches!(return_node.kind, NodeKind::Return { value: None }));
assert!(matches!(break_node.kind, NodeKind::Break { value: None }));
assert!(matches!(continue_node.kind, NodeKind::Continue));
}
#[test]
fn ownership_nodes() {
let expr = Box::new(make_node(
1,
NodeKind::Identifier {
name: dummy_ident("x"),
},
));
let mv = make_node(0, NodeKind::Move { expr: expr.clone() });
let borrow = make_node(2, NodeKind::Borrow { expr: expr.clone() });
let mut_borrow = make_node(3, NodeKind::MutableBorrow { expr: expr.clone() });
assert!(matches!(mv.kind, NodeKind::Move { .. }));
assert!(matches!(borrow.kind, NodeKind::Borrow { .. }));
assert!(matches!(mut_borrow.kind, NodeKind::MutableBorrow { .. }));
}
#[test]
fn effect_nodes() {
let handler = Box::new(make_node(
1,
NodeKind::Identifier {
name: dummy_ident("h"),
},
));
let body = Box::new(make_node(
2,
NodeKind::Block {
stmts: vec![],
tail: None,
},
));
let tp = TypePath {
segments: vec![dummy_ident("Log")],
span: dummy_span(),
};
let handling = make_node(
0,
NodeKind::HandlingBlock {
handlers: vec![AirHandlerPair {
effect: tp.clone(),
handler,
}],
body,
},
);
let effect_ref = make_node(3, NodeKind::EffectRef { path: tp });
assert!(matches!(handling.kind, NodeKind::HandlingBlock { .. }));
assert!(matches!(effect_ref.kind, NodeKind::EffectRef { .. }));
}
#[test]
fn type_expr_nodes() {
let named = make_node(
0,
NodeKind::TypeNamed {
path: TypePath {
segments: vec![dummy_ident("Int")],
span: dummy_span(),
},
args: vec![],
},
);
let self_ty = make_node(1, NodeKind::TypeSelf);
let opt = make_node(
2,
NodeKind::TypeOptional {
inner: Box::new(named.clone()),
},
);
assert!(matches!(named.kind, NodeKind::TypeNamed { .. }));
assert!(matches!(self_ty.kind, NodeKind::TypeSelf));
assert!(matches!(opt.kind, NodeKind::TypeOptional { .. }));
}
#[test]
fn metadata_and_effects_mutable() {
let mut node = make_node(0, NodeKind::Continue);
node.metadata
.insert("pass".into(), crate::stubs::Value::String("T-AIR".into()));
node.effects.insert(EffectRef::new("Std.Io.Log"));
node.capabilities
.insert(Capability::new("Std.Io.FileSystem"));
assert_eq!(node.metadata.len(), 1);
assert_eq!(node.effects.len(), 1);
assert_eq!(node.capabilities.len(), 1);
}
}