diff --git a/crates/flux-utils/src/arrayvec.rs b/crates/flux-utils/src/arrayvec.rs index 59936648..92b478b4 100644 --- a/crates/flux-utils/src/arrayvec.rs +++ b/crates/flux-utils/src/arrayvec.rs @@ -494,6 +494,16 @@ impl TypeHash for ArrayVec { }; } +impl TypeHash for ArrayStr { + const TYPE_HASH: u64 = { + let mut h = 0xcbf2_9ce4_8422_2325u64; + h = fnv1a64_str(h, "ArrayStr"); + h = hash_u64(h, N as u64); + h = hash_layout_of::(h); + h + }; +} + mod serde_impl { use core::fmt; diff --git a/crates/flux-versioned-types-macros/src/evolve/generate.rs b/crates/flux-versioned-types-macros/src/evolve/generate.rs index 9aa38e31..7c069ed0 100644 --- a/crates/flux-versioned-types-macros/src/evolve/generate.rs +++ b/crates/flux-versioned-types-macros/src/evolve/generate.rs @@ -4,7 +4,7 @@ use rustc_hash::FxHashMap; use syn::{Attribute, Ident, Type, Visibility}; use super::parse::{AddField, EvolutionOp, EvolveInput, EvolveStruct, ModifyField, StructField}; -use crate::shared::is_closure; +use crate::shared::{is_closure, without_schema_attrs}; pub(crate) struct FieldInfo { pub attrs: Vec, @@ -73,12 +73,13 @@ pub(crate) fn generate_struct_def( } pub(crate) fn generate_base_struct(input: &EvolveInput) -> (TokenStream2, Vec) { + let is_final = input.evolutions.is_empty(); let fields: Vec<_> = input .base .items .iter() .map(|f| { - let attrs = &f.attrs; + let attrs = emitted_attrs(&f.attrs, is_final); let vis = &f.vis; let name = &f.name; let ty = &f.ty; @@ -94,9 +95,17 @@ pub(crate) fn generate_base_struct(input: &EvolveInput) -> (TokenStream2, Vec Vec { + if is_final { attrs.to_vec() } else { without_schema_attrs(attrs) } +} + fn generate_evolved_struct_fields( kept_fields: &[&FieldInfo], ctx: &EvolutionContext, + is_final: bool, ) -> Vec { kept_fields .iter() @@ -106,13 +115,13 @@ fn generate_evolved_struct_fields( ctx.modify_map.get(&name_str).map_or_else( || { - let attrs = &f.attrs; + let attrs = emitted_attrs(&f.attrs, is_final); let vis = &f.vis; let ty = &f.ty; quote! { #(#attrs)* #vis #name: #ty } }, |modify| { - let attrs = &modify.attrs; + let attrs = emitted_attrs(&modify.attrs, is_final); let vis = &f.vis; let ty = &modify.new_ty; quote! { #(#attrs)* #vis #name: #ty } @@ -120,7 +129,7 @@ fn generate_evolved_struct_fields( ) }) .chain(ctx.add_fields.iter().map(|f| { - let attrs = &f.attrs; + let attrs = emitted_attrs(&f.attrs, is_final); let vis = &f.vis; let name = &f.name; let ty = &f.ty; @@ -222,13 +231,14 @@ pub(crate) fn generate_evolution( default_attrs: &[Attribute], current_fields: &[FieldInfo], prev_name: &Ident, + is_final: bool, ) -> (TokenStream2, Vec) { let ctx = EvolutionContext::from_evolution(evolution); let kept_fields: Vec<_> = current_fields.iter().filter(|f| !ctx.remove_names.contains(&f.name.to_string())).collect(); - let struct_fields = generate_evolved_struct_fields(&kept_fields, &ctx); + let struct_fields = generate_evolved_struct_fields(&kept_fields, &ctx, is_final); let struct_def = generate_struct_def(&evolution.name, default_attrs, &evolution.attrs, &struct_fields); let into_impl = generate_into_impl(prev_name, &evolution.name, &kept_fields, &ctx); diff --git a/crates/flux-versioned-types-macros/src/evolve_enum/generate.rs b/crates/flux-versioned-types-macros/src/evolve_enum/generate.rs index a989af8a..7f442569 100644 --- a/crates/flux-versioned-types-macros/src/evolve_enum/generate.rs +++ b/crates/flux-versioned-types-macros/src/evolve_enum/generate.rs @@ -6,18 +6,22 @@ use syn::{Attribute, Expr, Ident, Index}; use super::parse::{ EnumVariant, EvolveEnum, EvolveEnumInput, EvolveEnumOp, ModifyVariant, RemoveVariant, }; -use crate::shared::is_closure; +use crate::shared::{is_closure, without_schema_attrs}; fn generate_enum_def( name: &Ident, default_attrs: &[Attribute], enum_attrs: &[Attribute], variants: &[EnumVariant], + is_final: bool, ) -> TokenStream2 { + let emit_attrs = |attrs: &[Attribute]| { + if is_final { attrs.to_vec() } else { without_schema_attrs(attrs) } + }; let variant_tokens: Vec<_> = variants .iter() .map(|v| { - let vattrs = &v.attrs; + let vattrs = emit_attrs(&v.attrs); let vname = &v.name; let disc = v.discriminant.as_ref().map(|d| quote! { = #d }); if v.is_unit() { @@ -27,7 +31,7 @@ fn generate_enum_def( .fields .iter() .map(|f| { - let fattrs = &f.attrs; + let fattrs = emit_attrs(&f.attrs); let ty = &f.ty; quote! { #(#fattrs)* #ty } }) @@ -214,6 +218,7 @@ pub(crate) fn generate_base_enum(input: &EvolveEnumInput) -> (TokenStream2, Vec< &input.default_attrs, &input.base.attrs, &input.base.items, + input.evolutions.is_empty(), ); (output, input.base.items.clone()) @@ -224,6 +229,7 @@ pub(crate) fn generate_evolution( default_attrs: &[Attribute], current_variants: &[EnumVariant], prev_name: &Ident, + is_final: bool, ) -> (TokenStream2, Vec) { let mut add_variants = Vec::new(); let mut remove_map = FxHashMap::default(); @@ -274,8 +280,13 @@ pub(crate) fn generate_evolution( new_variants.push(av.clone()); } - let enum_def = - generate_enum_def(&evolution.name, default_attrs, &evolution.attrs, &new_variants); + let enum_def = generate_enum_def( + &evolution.name, + default_attrs, + &evolution.attrs, + &new_variants, + is_final, + ); let into_impl = generate_into_impl( prev_name, &evolution.name, diff --git a/crates/flux-versioned-types-macros/src/shared/generate.rs b/crates/flux-versioned-types-macros/src/shared/generate.rs index 36147fd8..2c0f0586 100644 --- a/crates/flux-versioned-types-macros/src/shared/generate.rs +++ b/crates/flux-versioned-types-macros/src/shared/generate.rs @@ -45,10 +45,19 @@ impl Named for EvolveBlock { } } +/// Schema-only attributes on fields and variants describe the final, +/// queryable shape, so they are stripped from every older version's +/// expansion: only the version carrying the schema derive may name them. +/// Version-level attributes are left alone, so an explicit whole-type proxy +/// keeps working on any version. +pub(crate) fn without_schema_attrs(attrs: &[Attribute]) -> Vec { + attrs.iter().filter(|attr| !attr.path().is_ident("telemetry_schema")).cloned().collect() +} + pub(crate) fn generate_evolving( input: &mut EvolveInputGeneric, generate_base: impl FnOnce(&EvolveInputGeneric) -> (TokenStream2, Vec), - generate_step: impl Fn(&E, &[Attribute], &[Item], &Ident) -> (TokenStream2, Vec), + generate_step: impl Fn(&E, &[Attribute], &[Item], &Ident, bool) -> (TokenStream2, Vec), ) -> TokenStream2 { if input.evolutions.is_empty() { input.base.attrs_mut().extend(input.final_attrs.clone()); @@ -84,9 +93,10 @@ pub(crate) fn generate_evolving( let mut output = base_output; let mut prev_name = input.base.name().clone(); - for evolution in &input.evolutions { + for (index, evolution) in input.evolutions.iter().enumerate() { + let is_final = index + 1 == input.evolutions.len(); let (ev_output, new_items) = - generate_step(evolution, &input.default_attrs, ¤t, &prev_name); + generate_step(evolution, &input.default_attrs, ¤t, &prev_name, is_final); output.extend(ev_output); current = new_items; prev_name = evolution.name().clone(); diff --git a/crates/flux-versioned-types-macros/src/shared/mod.rs b/crates/flux-versioned-types-macros/src/shared/mod.rs index 07fe97f2..6f6d93f3 100644 --- a/crates/flux-versioned-types-macros/src/shared/mod.rs +++ b/crates/flux-versioned-types-macros/src/shared/mod.rs @@ -2,6 +2,6 @@ mod generate; mod parse; pub(crate) use generate::{ - default_enum_attrs, default_struct_attrs, generate_evolving, is_closure, + default_enum_attrs, default_struct_attrs, generate_evolving, is_closure, without_schema_attrs, }; pub(crate) use parse::{BaseBlock, EvolveBlock, EvolveInputGeneric, ParseEvolveOp}; diff --git a/crates/flux-versioned-types/src/lib.rs b/crates/flux-versioned-types/src/lib.rs index ac6a9c25..448a4a64 100644 --- a/crates/flux-versioned-types/src/lib.rs +++ b/crates/flux-versioned-types/src/lib.rs @@ -92,6 +92,10 @@ macro_rules! __versioned_enum_inner { /// Like [`versioned_struct`], but the latest version also derives /// [`TelemetrySchema`] so it is queryable. With `persist = "dir"` the type /// also gets a [`VersionedPersistable`] home under that directory. +/// +/// Schema-only field attributes (`#[telemetry_schema(..)]`) may be written +/// on any version; they describe the final shape and are stripped from older +/// versions' expansions. #[macro_export] macro_rules! versioned_telemetry { ($name:ident, persist = $dir:expr => $($tokens:tt)*) => {