Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/kirin-derive-chumsky/src/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,7 @@ mod tests {
ident: Some(syn::Ident::new(name, proc_macro2::Span::call_site())),
collection: Collection::Single,
data: FieldData::Argument {
ssa_type: syn::parse_quote!(Placeholder::placeholder()),
ssa_type: Some(syn::parse_quote!(Placeholder::placeholder())),
},
}
}
Expand Down
26 changes: 26 additions & 0 deletions crates/kirin-derive-ir/src/tests/dialect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -467,3 +467,29 @@ fn test_dialect_derive_enum_mixed_callable_body_and_wraps() {
};
insta::assert_snapshot!(generate_dialect_code(input));
}

#[test]
fn operand_resolution_bound_only_for_checked_fields() {
for field in [
syn::parse_quote!(input: SSAValue),
syn::parse_quote!(#[kirin(type = T::placeholder())] input: Vec<SSAValue>),
syn::parse_quote!(#[kirin(type = T::placeholder())] input: Option<SSAValue>),
] {
let field: syn::Field = field;
let input = syn::parse_quote! {
#[kirin(type = T, builders)]
struct Op { #field }
};
assert!(!generate_dialect_code(input).contains("TypeResolution"));
}
let input = syn::parse_quote! {
#[kirin(type = T, builders, crate = custom_ir)]
struct Op {
#[kirin(type = T::placeholder())]
input: SSAValue,
}
};
let code = generate_dialect_code(input);
assert!(code.contains("Lang::Type: custom_ir::TypeResolution"));
assert!(code.contains("custom_ir::TypeResolution::is_resolved"));
}
4 changes: 4 additions & 0 deletions crates/kirin-derive-toolkit/src/ir/attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ pub struct KirinFieldOptions {
#[darling(default)]
pub into: bool,
pub default: Option<DefaultValue>,
/// SSA type expression. On a `ResultValue` it sets the result's type; on an
/// `SSAValue` the generated builder asserts the operand has this type.
/// Fields named in the expression are moved, so a non-`Copy` field must
/// be written as `field.clone()`.
#[darling(rename = "type")]
pub ssa_ty: Option<syn::Expr>,
}
Expand Down
5 changes: 4 additions & 1 deletion crates/kirin-derive-toolkit/src/ir/fields/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,11 @@ impl FieldCategory {
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum FieldData<L: Layout> {
/// The SSA input value. The `ssa_type` is `None` when the field does not have
/// `#[kirin(type = ...)]`. This results in not verifying the input type. Does not
/// default to `T::Placeholder`.
Argument {
ssa_type: syn::Expr,
ssa_type: Option<syn::Expr>,
},
/// SSA output value. When the user omits `#[kirin(type = ...)]`, `ssa_type`
/// defaults to `ir_type::placeholder()` (from the enum/struct-level
Expand Down
22 changes: 13 additions & 9 deletions crates/kirin-derive-toolkit/src/ir/fields/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use super::{Collection, FieldCategory, FieldData};
///
/// ```ignore
/// match field.category() {
/// FieldCategory::Argument => { /* field.ssa_type() is Some */ }
/// FieldCategory::Argument => { /* field.ssa_type() is Some only if annotated */ }
/// FieldCategory::Value => { /* field.value_type() is Some */ }
/// _ => {}
/// }
Expand Down Expand Up @@ -99,9 +99,11 @@ impl<L: Layout> FieldInfo<L> {
}

/// Return the SSA type expression for `Argument` or `Result` fields.
/// An argument without `#[kirin(type = ...)]` returns `None`.
pub fn ssa_type(&self) -> Option<&syn::Expr> {
match &self.data {
FieldData::Argument { ssa_type } | FieldData::Result { ssa_type, .. } => Some(ssa_type),
FieldData::Argument { ssa_type } => ssa_type.as_ref(),
FieldData::Result { ssa_type, .. } => Some(ssa_type),
_ => None,
}
}
Expand Down Expand Up @@ -160,7 +162,7 @@ mod tests {
ident: name.map(|n| syn::Ident::new(n, Span::call_site())),
collection: Collection::Single,
data: FieldData::Argument {
ssa_type: syn::parse_quote!(Placeholder::placeholder()),
ssa_type: Some(syn::parse_quote!(Placeholder::placeholder())),
},
}
}
Expand Down Expand Up @@ -222,12 +224,7 @@ mod tests {
#[test]
fn kind_name_all_categories() {
let cases: Vec<(FieldData<StandardLayout>, &str)> = vec![
(
FieldData::Argument {
ssa_type: syn::parse_quote!(()),
},
"argument",
),
(FieldData::Argument { ssa_type: None }, "argument"),
(
FieldData::Result {
ssa_type: syn::parse_quote!(()),
Expand Down Expand Up @@ -317,6 +314,13 @@ mod tests {
assert!(f.ssa_type().is_some());
}

#[test]
fn ssa_type_on_unannotated_argument() {
let mut f = make_argument_field(0, Some("x"));
f.data = FieldData::Argument { ssa_type: None };
assert!(f.ssa_type().is_none());
}

#[test]
fn ssa_type_on_result() {
let f = make_result_field(0, "out");
Expand Down
4 changes: 1 addition & 3 deletions crates/kirin-derive-toolkit/src/ir/statement/definition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,7 @@ impl<L: Layout> Statement<L> {
ident,
collection,
data: FieldData::Argument {
ssa_type: kirin_opts
.ssa_ty
.unwrap_or_else(|| syn::parse_quote! { () }),
ssa_type: kirin_opts.ssa_ty,
},
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,33 @@ fn statement_id_name(info: &StatementInfo) -> syn::Ident {
format_ident!("{}_statement_id", name, span = info.name.span())
}

fn build_fn_arg_type_checks(
info: &StatementInfo,
crate_path: &syn::Path,
) -> Vec<proc_macro2::TokenStream> {
info.fields
.iter()
.filter(|field| {
field.category() == FieldCategory::Argument
&& matches!(field.collection, Collection::Single)
})
.filter_map(|field| {
let expected = field.ssa_type()?;
let name = field.name_ident(info.name.span());
let label = format!("{}::{}", info.name, field);
Some(quote! {
if let Some(__actual) = stage.ssa_arena().get(#name).and_then(|i| i.ty()) {
if #crate_path::TypeResolution::is_resolved(__actual) {
let __expected = Lang::Type::from(#expected);
if #crate_path::TypeResolution::is_resolved(&__expected) {
assert!(*__actual == __expected, "{}: expected operand of type {:?}, got {:?}", #label, __expected, __actual);
}
}
}
})
}).collect()
}

fn build_fn_body(
info: &StatementInfo,
input: &InputMeta,
Expand All @@ -269,6 +296,7 @@ fn build_fn_body(
) -> proc_macro2::TokenStream {
let statement_id = statement_id_name(info);
let let_inputs = build_fn_let_inputs(info, &input.ir_type);
let arg_type_checks = build_fn_arg_type_checks(info, crate_path);
let let_results = let_name_eq_result_value(info, result_name_map, crate_path);

let is_tuple = info.fields.iter().all(|f| f.ident.is_none());
Expand Down Expand Up @@ -306,6 +334,7 @@ fn build_fn_body(
quote! {{
let #statement_id = stage.statement_arena().next_id();
#(#let_inputs)*
#(#arg_type_checks)*
#let_results

stage
Expand Down Expand Up @@ -441,6 +470,16 @@ pub(super) fn build_fn_for_statement(
let body = build_fn_body(info, input, &result_name_map, crate_path);
let self_ty = quote! { #name #ty_generics };

let resolution_bound = if info.fields.iter().any(|field| {
field.category() == FieldCategory::Argument
&& matches!(field.collection, Collection::Single)
&& field.ssa_type().is_some()
}) {
quote! { , Lang::Type: #crate_path::TypeResolution }
} else {
quote! {}
};

let needs_placeholder_bound = info.fields.iter().any(|f| f.is_auto_placeholder());

let placeholder_bound = if needs_placeholder_bound {
Expand All @@ -457,6 +496,7 @@ pub(super) fn build_fn_for_statement(
Lang: #crate_path::Dialect + ::core::convert::From<#self_ty>,
Lang::Type: From<#ir_type>
#placeholder_bound
#resolution_bound
{
let stage = stage.as_build_stage();
#body
Expand Down Expand Up @@ -753,7 +793,7 @@ mod tests {
ident: Some(syn::Ident::new(name, proc_macro2::Span::call_site())),
collection: Collection::Single,
data: FieldData::Argument {
ssa_type: syn::parse_quote!(MyType::placeholder()),
ssa_type: Some(syn::parse_quote!(MyType::placeholder())),
},
}
}
Expand Down
8 changes: 8 additions & 0 deletions crates/kirin-ir/src/comptime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ pub trait Placeholder: CompileTimeValue {
fn placeholder() -> Self;
}

/// Whether a type is ready for equality checks in builders.
///
/// Return false for unknown types or unresolved components.
/// Skipped checks are not retried after inference.
pub trait TypeResolution {
fn is_resolved(&self) -> bool;
}

pub trait Typeof<Ty> {
fn type_of(&self) -> Ty;
}
Expand Down
4 changes: 2 additions & 2 deletions crates/kirin-ir/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ pub use builder::error::{
StagedFunctionConflictKind, StagedFunctionError,
};
pub use builder::{AsBuildStage, BuilderStageInfo, FinalizeError};
pub use comptime::{CompileTimeValue, Placeholder, Typeof};
pub use comptime::{CompileTimeValue, Placeholder, TypeResolution, Typeof};
pub use derived::{
BlockBody, ChainDefect, ChainFinding, DanglingParent, DeriveError, Finding, Mismatch,
VerifyError, verify_derived,
Expand Down Expand Up @@ -73,7 +73,7 @@ pub mod prelude {
};
pub use crate::{
CompileTimeValue, HasProduct, Placeholder, Product, Project, ProjectError, TryProject,
TryProjectTo, Typeof,
TryProjectTo, TypeResolution, Typeof,
};
}

Expand Down
4 changes: 4 additions & 0 deletions crates/kirin-test-languages/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ mod namespaced_language;
mod scalar_function_language;
#[cfg(feature = "simple-language")]
mod simple_language;
#[cfg(feature = "simple-language")]
mod typed_operand_language;
#[cfg(feature = "ungraph-language")]
mod ungraph_language;

Expand All @@ -36,6 +38,8 @@ pub use namespaced_language::NamespacedLanguage;
pub use scalar_function_language::ScalarFunctionLanguage;
#[cfg(feature = "simple-language")]
pub use simple_language::SimpleLanguage;
#[cfg(feature = "simple-language")]
pub use typed_operand_language::TypedOperandLanguage;
#[cfg(feature = "ungraph-language")]
pub use ungraph_language::{
UngraphCompound, UngraphEdge, UngraphLanguage, UngraphNodeA, UngraphNodeB,
Expand Down
22 changes: 22 additions & 0 deletions crates/kirin-test-languages/src/typed_operand_language.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use crate::SimpleType;
use kirin_ir::{Dialect, ResultValue, SSAValue};

/// Operands with `#[kirin(type = ...)]`, so the generated builders check them.
#[derive(Clone, Debug, PartialEq, Dialect)]
#[kirin(builders, type = SimpleType, crate = kirin_ir)]
pub enum TypedOperandLanguage {
/// The first operand must be `I64`. The second has no annotation and is never checked.
Index(
#[kirin(type = SimpleType::I64)] SSAValue,
SSAValue,
#[kirin(type = SimpleType::F64)] ResultValue,
),
/// The expected type comes from another field (`SimpleType` isn't `Copy`).
Cast {
#[kirin(type = target.clone())]
input: SSAValue,
target: SimpleType,
#[kirin(type = target.clone())]
result: ResultValue,
},
}
7 changes: 7 additions & 0 deletions crates/kirin-test-types/src/simple_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ pub enum SimpleType {
Bottom,
}

impl kirin_ir::TypeResolution for SimpleType {
fn is_resolved(&self) -> bool {
// Bottom is unresolved; Any is resolved.
!matches!(self, Self::Bottom)
}
}

impl Lattice for SimpleType {
fn is_subseteq(&self, other: &Self) -> bool {
self == other || matches!(other, SimpleType::Any) || matches!(self, SimpleType::Bottom)
Expand Down
74 changes: 74 additions & 0 deletions tests/builder_operand_types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//! Generated builders check annotated `SSAValue` operands against their
//! `#[kirin(type = ...)]`, and skip operands whose type isn't set yet.

use kirin::prelude::*;
use kirin_ir::BuilderSSAKind;
use kirin_test_languages::{SimpleType, TypedOperandLanguage as L};

fn typed(stage: &mut BuilderStageInfo<L>, ty: SimpleType) -> SSAValue {
stage.ssa().ty(ty).kind(BuilderSSAKind::Test).new()
}

#[test]
fn accepts_operand_of_annotated_type() {
let mut stage = BuilderStageInfo::<L>::default();
let idx = typed(&mut stage, SimpleType::I64);
let any = typed(&mut stage, SimpleType::Bool); // unannotated, never checked
L::op_index(&mut stage, idx, any);
}

#[test]
fn accepts_untyped_placeholders() {
let mut stage = BuilderStageInfo::<L>::default();
let idx = stage.block_argument().index(0);
let port = stage.graph_port().index(0);
L::op_index(&mut stage, idx, port);
let port = stage.graph_port().name("x");
L::op_cast(&mut stage, port, SimpleType::F32);
}

#[test]
#[should_panic(expected = "Index::field_0: expected operand of type I64, got F32")]
fn rejects_operand_of_wrong_type() {
let mut stage = BuilderStageInfo::<L>::default();
let idx = typed(&mut stage, SimpleType::F32);
let any = typed(&mut stage, SimpleType::Bool);
L::op_index(&mut stage, idx, any);
}

#[test]
#[should_panic(expected = "Cast::input: expected operand of type F32, got I64")]
fn rejects_operand_against_field_computed_type() {
let mut stage = BuilderStageInfo::<L>::default();
let input = typed(&mut stage, SimpleType::I64);
L::op_cast(&mut stage, input, SimpleType::F32);
}

#[test]
fn accepts_unresolved_actual_type() {
let mut stage = BuilderStageInfo::<L>::default();
let input = typed(&mut stage, SimpleType::Bottom);
L::op_cast(&mut stage, input, SimpleType::I64);
}

#[test]
fn accepts_unresolved_expected_type() {
let mut stage = BuilderStageInfo::<L>::default();
let input = typed(&mut stage, SimpleType::I64);
L::op_cast(&mut stage, input, SimpleType::Bottom);
}

#[test]
fn accepts_both_types_unresolved() {
let mut stage = BuilderStageInfo::<L>::default();
let input = typed(&mut stage, SimpleType::Bottom);
L::op_cast(&mut stage, input, SimpleType::Bottom);
}

#[test]
#[should_panic(expected = "Cast::input: expected operand of type I64, got Any")]
fn resolved_top_type_is_still_checked() {
let mut stage = BuilderStageInfo::<L>::default();
let input = typed(&mut stage, SimpleType::Any);
L::op_cast(&mut stage, input, SimpleType::I64);
}
Loading