diff --git a/crates/kirin-derive-chumsky/src/validation.rs b/crates/kirin-derive-chumsky/src/validation.rs index 2138bc7a7..b02d75ba6 100644 --- a/crates/kirin-derive-chumsky/src/validation.rs +++ b/crates/kirin-derive-chumsky/src/validation.rs @@ -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())), }, } } diff --git a/crates/kirin-derive-ir/src/tests/dialect.rs b/crates/kirin-derive-ir/src/tests/dialect.rs index 308c73117..cbe0eadd9 100644 --- a/crates/kirin-derive-ir/src/tests/dialect.rs +++ b/crates/kirin-derive-ir/src/tests/dialect.rs @@ -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), + syn::parse_quote!(#[kirin(type = T::placeholder())] input: Option), + ] { + 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")); +} diff --git a/crates/kirin-derive-toolkit/src/ir/attrs.rs b/crates/kirin-derive-toolkit/src/ir/attrs.rs index 32d054f5d..195265e4a 100644 --- a/crates/kirin-derive-toolkit/src/ir/attrs.rs +++ b/crates/kirin-derive-toolkit/src/ir/attrs.rs @@ -77,6 +77,10 @@ pub struct KirinFieldOptions { #[darling(default)] pub into: bool, pub default: Option, + /// 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, } diff --git a/crates/kirin-derive-toolkit/src/ir/fields/data.rs b/crates/kirin-derive-toolkit/src/ir/fields/data.rs index d1736a279..23a85dc10 100644 --- a/crates/kirin-derive-toolkit/src/ir/fields/data.rs +++ b/crates/kirin-derive-toolkit/src/ir/fields/data.rs @@ -41,8 +41,11 @@ impl FieldCategory { #[derive(Debug)] #[allow(clippy::large_enum_variant)] pub enum FieldData { + /// 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, }, /// SSA output value. When the user omits `#[kirin(type = ...)]`, `ssa_type` /// defaults to `ir_type::placeholder()` (from the enum/struct-level diff --git a/crates/kirin-derive-toolkit/src/ir/fields/info.rs b/crates/kirin-derive-toolkit/src/ir/fields/info.rs index b3a095ff0..82f36afa0 100644 --- a/crates/kirin-derive-toolkit/src/ir/fields/info.rs +++ b/crates/kirin-derive-toolkit/src/ir/fields/info.rs @@ -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 */ } /// _ => {} /// } @@ -99,9 +99,11 @@ impl FieldInfo { } /// 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, } } @@ -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())), }, } } @@ -222,12 +224,7 @@ mod tests { #[test] fn kind_name_all_categories() { let cases: Vec<(FieldData, &str)> = vec![ - ( - FieldData::Argument { - ssa_type: syn::parse_quote!(()), - }, - "argument", - ), + (FieldData::Argument { ssa_type: None }, "argument"), ( FieldData::Result { ssa_type: syn::parse_quote!(()), @@ -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"); diff --git a/crates/kirin-derive-toolkit/src/ir/statement/definition.rs b/crates/kirin-derive-toolkit/src/ir/statement/definition.rs index 88a3065dd..0ef6052f6 100644 --- a/crates/kirin-derive-toolkit/src/ir/statement/definition.rs +++ b/crates/kirin-derive-toolkit/src/ir/statement/definition.rs @@ -217,9 +217,7 @@ impl Statement { ident, collection, data: FieldData::Argument { - ssa_type: kirin_opts - .ssa_ty - .unwrap_or_else(|| syn::parse_quote! { () }), + ssa_type: kirin_opts.ssa_ty, }, }); } diff --git a/crates/kirin-derive-toolkit/src/template/builder_template/helpers.rs b/crates/kirin-derive-toolkit/src/template/builder_template/helpers.rs index 61961fc61..596b9835a 100644 --- a/crates/kirin-derive-toolkit/src/template/builder_template/helpers.rs +++ b/crates/kirin-derive-toolkit/src/template/builder_template/helpers.rs @@ -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 { + 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, @@ -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()); @@ -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 @@ -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 { @@ -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 @@ -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())), }, } } diff --git a/crates/kirin-ir/src/comptime.rs b/crates/kirin-ir/src/comptime.rs index 61ebc6507..f52aeadc6 100644 --- a/crates/kirin-ir/src/comptime.rs +++ b/crates/kirin-ir/src/comptime.rs @@ -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 { fn type_of(&self) -> Ty; } diff --git a/crates/kirin-ir/src/lib.rs b/crates/kirin-ir/src/lib.rs index ece86535c..012f9a99b 100644 --- a/crates/kirin-ir/src/lib.rs +++ b/crates/kirin-ir/src/lib.rs @@ -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, @@ -73,7 +73,7 @@ pub mod prelude { }; pub use crate::{ CompileTimeValue, HasProduct, Placeholder, Product, Project, ProjectError, TryProject, - TryProjectTo, Typeof, + TryProjectTo, TypeResolution, Typeof, }; } diff --git a/crates/kirin-test-languages/src/lib.rs b/crates/kirin-test-languages/src/lib.rs index a60b06952..0be9e6fd4 100644 --- a/crates/kirin-test-languages/src/lib.rs +++ b/crates/kirin-test-languages/src/lib.rs @@ -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; @@ -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, diff --git a/crates/kirin-test-languages/src/typed_operand_language.rs b/crates/kirin-test-languages/src/typed_operand_language.rs new file mode 100644 index 000000000..9408e9f00 --- /dev/null +++ b/crates/kirin-test-languages/src/typed_operand_language.rs @@ -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, + }, +} diff --git a/crates/kirin-test-types/src/simple_type.rs b/crates/kirin-test-types/src/simple_type.rs index 3976cb9df..dc7806d2f 100644 --- a/crates/kirin-test-types/src/simple_type.rs +++ b/crates/kirin-test-types/src/simple_type.rs @@ -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) diff --git a/tests/builder_operand_types.rs b/tests/builder_operand_types.rs new file mode 100644 index 000000000..a7ca18f49 --- /dev/null +++ b/tests/builder_operand_types.rs @@ -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, ty: SimpleType) -> SSAValue { + stage.ssa().ty(ty).kind(BuilderSSAKind::Test).new() +} + +#[test] +fn accepts_operand_of_annotated_type() { + let mut stage = BuilderStageInfo::::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::::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::::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::::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::::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::::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::::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::::default(); + let input = typed(&mut stage, SimpleType::Any); + L::op_cast(&mut stage, input, SimpleType::I64); +}