Skip to main content

triblespace_macros_common/
lib.rs

1use proc_macro2::Span;
2use proc_macro2::TokenStream as TokenStream2;
3use quote::format_ident;
4use quote::quote;
5use quote::ToTokens;
6use syn::braced;
7use syn::bracketed;
8use syn::parse::Parse;
9use syn::parse::ParseStream;
10use syn::punctuated::Punctuated;
11use syn::Expr;
12use syn::Ident;
13use syn::Token;
14
15mod attributes;
16mod find;
17mod value_formatter;
18
19pub use attributes::attributes_impl;
20pub use find::find_impl;
21pub use value_formatter::value_formatter_impl;
22
23struct PatternInput {
24    set: Expr,
25    pattern: Vec<Entity>,
26}
27
28struct Entity {
29    id: Option<Inline>,
30    attributes: Vec<Attribute>,
31}
32
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
34enum AttributeMode {
35    Required,
36    Optional,
37    Repeated,
38}
39
40enum Inline {
41    Var(Ident),
42    LocalVar(Ident),
43    Expr(Expr),
44}
45
46struct Attribute {
47    /// Attribute identifier in the pattern's `name: value` slot.
48    /// Three shapes are supported:
49    ///   - `Inline::Expr(e)`     — known attribute constant, e.g.
50    ///                            `cwork_title`. The macro resolves
51    ///                            the attribute Id and uses its
52    ///                            schema to type the value.
53    ///   - `Inline::Var(ident)`  — free attribute, bound to a
54    ///                            `find!`-projected
55    ///                            `Variable<GenId>`. The value
56    ///                            position is required to be a
57    ///                            `Variable<UnknownInline>` — the
58    ///                            macro emits a compile-time type
59    ///                            assertion to that effect.
60    ///                            Without a fixed predicate the
61    ///                            value bytes can come from any
62    ///                            schema, so the user must take an
63    ///                            explicit `try_from_inline::<S>()`
64    ///                            step to decode them and that
65    ///                            transmutation has to live at the
66    ///                            use site rather than the
67    ///                            projection.
68    ///   - `Inline::LocalVar(_)` — pattern-local helper variable in
69    ///                            the attribute slot. Free-attribute
70    ///                            in the value slot is rejected
71    ///                            (no schema to infer); the user
72    ///                            should project a `Variable<UnknownInline>`
73    ///                            via `find!` instead.
74    name: Inline,
75    mode: AttributeMode,
76    value: Inline,
77}
78
79impl Parse for PatternInput {
80    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
81        let set: Expr = input.parse()?;
82        input.parse::<Token![,]>()?;
83        let content;
84        bracketed!(content in input);
85
86        let pattern = Punctuated::<_, Token![,]>::parse_terminated(&content)?
87            .into_iter()
88            .collect();
89
90        Ok(PatternInput { set, pattern })
91    }
92}
93
94impl Parse for Entity {
95    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
96        let content;
97        braced!(content in input);
98
99        let mut id: Option<Inline> = None;
100        {
101            let fork = content.fork();
102            // Special-case `_ @` so callers can be explicit about content-derived identity
103            // without colliding with a Rust identifier.
104            if fork.peek(Token![_]) {
105                let fork2 = fork;
106                fork2.parse::<Token![_]>()?;
107                if fork2.peek(Token![@]) {
108                    content.parse::<Token![_]>()?;
109                    content.parse::<Token![@]>()?;
110                }
111            }
112        }
113        if id.is_none() {
114            let fork = content.fork();
115            if fork.parse::<Inline>().is_ok() && fork.peek(Token![@]) {
116                let pv: Inline = content.parse()?;
117                content.parse::<Token![@]>()?;
118                id = Some(pv);
119            }
120        }
121
122        let attributes = Punctuated::<_, Token![,]>::parse_terminated(&content)?
123            .into_iter()
124            .collect();
125
126        Ok(Entity { id, attributes })
127    }
128}
129
130impl Parse for Attribute {
131    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
132        // Support explicit fact modifiers while keeping `attr: value` flexible:
133        // - `attr: value`   (required, attr can be any expression OR `?ident` / `_?ident`)
134        // - `attr?: value`  (optional, value must be `Option<T>`)
135        // - `attr*: value`  (repeated, value must be an `IntoIterator<Item = T>`)
136        //
137        // The `?:` and `*:` modifiers only make sense for known
138        // attribute constants (Optional/Repeated need the
139        // attribute's schema to type the value), so they're parsed
140        // only after a path-like attribute reference. Free
141        // attributes (`?attr`, `_?attr`) are required-mode only.
142        let fork = input.fork();
143        if let Ok(expr_path) = fork.parse::<syn::ExprPath>() {
144            if fork.peek(Token![?]) {
145                let fork2 = fork.fork();
146                fork2.parse::<Token![?]>()?;
147                if fork2.peek(Token![:]) {
148                    let name = Inline::Expr(Expr::Path(input.parse::<syn::ExprPath>()?));
149                    input.parse::<Token![?]>()?;
150                    input.parse::<Token![:]>()?;
151                    let value: Inline = input.parse()?;
152                    return Ok(Attribute {
153                        name,
154                        mode: AttributeMode::Optional,
155                        value,
156                    });
157                }
158            }
159
160            if fork.peek(Token![*]) {
161                let fork2 = fork.fork();
162                fork2.parse::<Token![*]>()?;
163                if fork2.peek(Token![:]) {
164                    let name = Inline::Expr(Expr::Path(input.parse::<syn::ExprPath>()?));
165                    input.parse::<Token![*]>()?;
166                    input.parse::<Token![:]>()?;
167                    let value: Inline = input.parse()?;
168                    return Ok(Attribute {
169                        name,
170                        mode: AttributeMode::Repeated,
171                        value,
172                    });
173                }
174            }
175
176            // Path-like required fact: fall through to the generic
177            // Inline branch below, which also accepts `?ident` /
178            // `_?ident`.
179            let _ = expr_path;
180        }
181
182        let name: Inline = input.parse()?;
183        input.parse::<Token![:]>()?;
184        let value: Inline = input.parse()?;
185        Ok(Attribute {
186            name,
187            mode: AttributeMode::Required,
188            value,
189        })
190    }
191}
192
193impl Parse for Inline {
194    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
195        if input.peek(Token![_]) {
196            let fork = input.fork();
197            fork.parse::<Token![_]>()?;
198            if fork.peek(Token![?]) {
199                input.parse::<Token![_]>()?;
200                input.parse::<Token![?]>()?;
201                let var_ident: Ident = input.parse()?;
202                return Ok(Inline::LocalVar(var_ident));
203            }
204        }
205        if input.peek(Token![?]) {
206            input.parse::<Token![?]>()?;
207            let var_ident: Ident = input.parse()?;
208            Ok(Inline::Var(var_ident))
209        } else {
210            let expr: Expr = input.parse()?;
211            Ok(Inline::Expr(expr))
212        }
213    }
214}
215
216pub fn pattern_impl(input: TokenStream2, base_path: &TokenStream2) -> syn::Result<TokenStream2> {
217    let PatternInput { set, pattern } = syn::parse2(input)?;
218
219    let ctx_ident = format_ident!("__ctx", span = Span::mixed_site());
220    let set_ident = format_ident!("__set", span = Span::mixed_site());
221
222    let mut entity_tokens = TokenStream2::new();
223    let mut attr_tokens = TokenStream2::new();
224
225    use std::collections::HashMap;
226    let mut attr_idx = 0usize;
227    let mut val_idx = 0usize;
228    // Tuple in the map is (attribute variable ident, optional
229    // attribute-constant reference ident). The reference is None
230    // for free-attribute patterns (`?attr` / `_?attr`) where no
231    // `Attribute` constant is available.
232    let mut attr_map: HashMap<String, (Ident, Option<Ident>)> = HashMap::new();
233    let mut local_tokens = TokenStream2::new();
234    let mut local_map: HashMap<String, Ident> = HashMap::new();
235    let mut local_idx = 0usize;
236    let mut get_local_var = |ident: &Ident| {
237        let key = format!("_?{}", ident);
238        local_map
239            .entry(key)
240            .or_insert_with(|| {
241                let ident = format_ident!("__local{}", local_idx, span = Span::mixed_site());
242                local_idx += 1;
243                local_tokens.extend(quote! {
244                    let #ident = #ctx_ident.next_variable();
245                });
246                ident
247            })
248            .clone()
249    };
250
251    for (entity_idx, entity) in pattern.into_iter().enumerate() {
252        let e_ident = format_ident!("__e{}", entity_idx, span = Span::mixed_site());
253        // Track the entity's variable identity so we can detect
254        // self-referencing patterns like `{ _?e @ attr: _?e }` or
255        // `{ ?e @ attr: ?e }` and desugar them with EqualityConstraint.
256        let entity_var_key: Option<(bool, String)> = match entity.id {
257            Some(Inline::LocalVar(ref ident)) => Some((true, ident.to_string())),
258            Some(Inline::Var(ref ident)) => Some((false, ident.to_string())),
259            _ => None,
260        };
261        let init = if let Some(ref id_val) = entity.id {
262            match id_val {
263                Inline::Var(ref ident) => {
264                    quote! { let #e_ident = #ident; }
265                }
266                Inline::LocalVar(ref ident) => {
267                    let local_ident = get_local_var(ident);
268                    quote! { let #e_ident = #local_ident; }
269                }
270                Inline::Expr(ref id_expr) => {
271                    // Constant entity id: fold it into the pattern as a
272                    // constant term — no hidden variable, no
273                    // ConstantConstraint.
274                    quote! {
275                        let #e_ident: #base_path::inline::Inline<#base_path::inline::encodings::genid::GenId> = #base_path::inline::IntoInline::to_inline(#id_expr);
276                    }
277                }
278            }
279        } else {
280            quote! {
281                let #e_ident: #base_path::query::Variable<#base_path::inline::encodings::genid::GenId> = #ctx_ident.next_variable();
282            }
283        };
284        entity_tokens.extend(init);
285
286        for Attribute { name, mode, value } in entity.attributes {
287            if mode != AttributeMode::Required {
288                let span_for_err = match &name {
289                    Inline::Expr(e) => e.to_token_stream(),
290                    Inline::Var(i) => i.to_token_stream(),
291                    Inline::LocalVar(i) => i.to_token_stream(),
292                };
293                return Err(syn::Error::new_spanned(
294                    span_for_err,
295                    "`?:` and `*:` are not supported in pattern!; use `attr: value`",
296                ));
297            }
298
299            // Set up (or reuse) the attribute term for this slot.
300            // For Inline::Expr (concrete attribute) we emit a
301            // `let __af = &expr` reference to the Attribute constant
302            // (so downstream value codegen can call `.inline_from(...)`
303            // / `.as_variable(...)` on it) plus a constant
304            // `Inline<GenId>` term carrying the attribute id — no
305            // hidden variable is allocated, so the constant never
306            // appears in the constraint's variable set. For
307            // Inline::Var / Inline::LocalVar (free attribute) there is
308            // no schema available — `__af` is `None` and the value
309            // position must use the opaque `UnknownInline` schema.
310            let key = match &name {
311                Inline::Var(ident) => format!("?{}", ident),
312                Inline::LocalVar(ident) => format!("_?{}", ident),
313                Inline::Expr(expr) => expr.to_token_stream().to_string(),
314            };
315            let (a_var_ident, af_ident_opt) = attr_map
316                .entry(key)
317                .or_insert_with(|| {
318                    let a_ident =
319                        format_ident!("__a{}", attr_idx, span = Span::mixed_site());
320                    attr_idx += 1;
321                    match &name {
322                        Inline::Expr(expr) => {
323                            let af_ident = format_ident!(
324                                "__af{}",
325                                attr_idx,
326                                span = Span::mixed_site()
327                            );
328                            attr_tokens.extend(quote! {
329                                let #af_ident = &#expr;
330                                let #a_ident: #base_path::inline::Inline<#base_path::inline::encodings::genid::GenId> = #base_path::inline::IntoInline::to_inline(#af_ident.id());
331                            });
332                            (a_ident, Some(af_ident))
333                        }
334                        Inline::Var(user_ident) => {
335                            attr_tokens.extend(quote! {
336                                let #a_ident: #base_path::query::Variable<#base_path::inline::encodings::genid::GenId> = #user_ident;
337                            });
338                            (a_ident, None)
339                        }
340                        Inline::LocalVar(local_ident) => {
341                            let local_var = get_local_var(local_ident);
342                            attr_tokens.extend(quote! {
343                                let #a_ident: #base_path::query::Variable<#base_path::inline::encodings::genid::GenId> = #local_var;
344                            });
345                            (a_ident, None)
346                        }
347                    }
348                })
349                .clone();
350
351            let val_id = {
352                let v = val_idx;
353                val_idx += 1;
354                v
355            };
356            let v_tmp_ident = format_ident!("__v{}", val_id, span = Span::mixed_site());
357
358            // Self-reference detection (only meaningful for projected
359            // variables, where the entity and value bind the same
360            // user variable).
361            let value_var_key: Option<(bool, String)> = match value {
362                Inline::Var(ref ident) => Some((false, ident.to_string())),
363                Inline::LocalVar(ref ident) => Some((true, ident.to_string())),
364                _ => None,
365            };
366            let self_ref = entity_var_key.is_some() && entity_var_key == value_var_key;
367
368            // Emit the per-trible constraint. The shape splits along
369            // two axes: Inline variant (Var / LocalVar / Expr) and
370            // whether the attribute is concrete (`af_ident_opt =
371            // Some`) or free (`af_ident_opt = None`).
372            let triple_tokens = match (value, af_ident_opt.as_ref()) {
373                // ---- concrete attribute paths (existing) ----
374                (Inline::Var(ref var_ident), Some(af_ident)) if self_ref => {
375                    let alias_ident = format_ident!("__alias{}", val_id, span = Span::mixed_site());
376                    quote! {
377                        {
378                            #[allow(unused_imports)] use #base_path::query::TriblePattern;
379                            let #alias_ident: #base_path::query::Variable<#base_path::inline::encodings::genid::GenId> = #ctx_ident.next_variable();
380                            let v_var = #af_ident.as_variable(#alias_ident);
381                            constraints.push(Box::new(#base_path::query::equalityconstraint::EqualityConstraint::new(#e_ident.index, #alias_ident.index)));
382                            constraints.push(Box::new(#set_ident.pattern(#e_ident, #a_var_ident, v_var)));
383                        }
384                    }
385                }
386                (Inline::Var(ref var_ident), Some(af_ident)) => {
387                    quote! {
388                        {
389                            #[allow(unused_imports)] use #base_path::query::TriblePattern;
390                            let v_var = { #af_ident.as_variable(#var_ident) };
391                            constraints.push(Box::new(#set_ident.pattern(#e_ident, #a_var_ident, v_var)));
392                        }
393                    }
394                }
395                (Inline::LocalVar(ref var_ident), Some(af_ident)) if self_ref => {
396                    let _local_ident = get_local_var(var_ident);
397                    let alias_ident = format_ident!("__alias{}", val_id, span = Span::mixed_site());
398                    quote! {
399                        {
400                            #[allow(unused_imports)] use #base_path::query::TriblePattern;
401                            let #alias_ident: #base_path::query::Variable<#base_path::inline::encodings::genid::GenId> = #ctx_ident.next_variable();
402                            let v_var = #af_ident.as_variable(#alias_ident);
403                            constraints.push(Box::new(#base_path::query::equalityconstraint::EqualityConstraint::new(#e_ident.index, #alias_ident.index)));
404                            constraints.push(Box::new(#set_ident.pattern(#e_ident, #a_var_ident, v_var)));
405                        }
406                    }
407                }
408                (Inline::LocalVar(ref var_ident), Some(af_ident)) => {
409                    let local_ident = get_local_var(var_ident);
410                    quote! {
411                        {
412                            #[allow(unused_imports)] use #base_path::query::TriblePattern;
413                            let v_var = { #af_ident.as_variable(#local_ident) };
414                            constraints.push(Box::new(#set_ident.pattern(#e_ident, #a_var_ident, v_var)));
415                        }
416                    }
417                }
418                (Inline::Expr(ref expr), Some(af_ident)) => {
419                    // Literal value: fold it into the pattern as a
420                    // constant term (typed by the attribute's schema
421                    // via `inline_from`) — no hidden variable, no
422                    // ConstantConstraint.
423                    quote! {
424                        {
425                            #[allow(unused_imports)] use #base_path::query::TriblePattern;
426                            let #v_tmp_ident = #af_ident.inline_from(#expr);
427                            constraints.push(Box::new(#set_ident.pattern(#e_ident, #a_var_ident, #v_tmp_ident)));
428                        }
429                    }
430                }
431
432                // ---- free attribute paths (new) ----
433                //
434                // No Attribute constant is available, so the macro
435                // can't apply a schema cast and the engine is going
436                // to match bytes regardless of the schema the user
437                // typed the variable with. Letting any
438                // `Variable<S>` through would compile, the join
439                // would still find the right rows, but
440                // `try_from_inline::<S>()` on the result would lie
441                // for every row whose predicate isn't actually an
442                // `S`. To make that footgun a compile-time error,
443                // we emit a static type assertion that the user's
444                // variable is exactly `Variable<UnknownInline>` —
445                // forcing the receiver to do an explicit
446                // `try_from_inline::<RealSchema>()` at the use site
447                // when they know which predicate they're decoding.
448                (Inline::Var(ref var_ident), None) => {
449                    quote! {
450                        {
451                            #[allow(unused_imports)] use #base_path::query::TriblePattern;
452                            // Compile-time enforcement: the value variable
453                            // for a free-attribute pattern must be typed
454                            // `Variable<UnknownInline>`. Any other schema
455                            // is rejected here so users can't quietly
456                            // misinterpret the bytes downstream.
457                            let _: &#base_path::query::Variable<#base_path::inline::encodings::UnknownInline> = &#var_ident;
458                            constraints.push(Box::new(#set_ident.pattern(#e_ident, #a_var_ident, #var_ident)));
459                        }
460                    }
461                }
462                (Inline::LocalVar(ref var_ident), None) => {
463                    return Err(syn::Error::new_spanned(
464                        var_ident,
465                        "local helper variables (_?ident) in the value position are not supported with a free attribute (?attr); use a `find!`-projected `Variable<UnknownInline>` (`?val`) instead",
466                    ));
467                }
468                (Inline::Expr(ref expr), None) => {
469                    return Err(syn::Error::new_spanned(
470                        expr,
471                        "free attribute (?ident) requires a `Variable<UnknownInline>` query variable in the value position; concrete value expressions need a known attribute so the macro can apply its schema",
472                    ));
473                }
474            };
475            entity_tokens.extend(triple_tokens);
476        }
477    }
478
479    let output = quote! {
480        {
481            let mut constraints: ::std::vec::Vec<Box<dyn #base_path::query::Constraint + Send + Sync>> = ::std::vec::Vec::new();
482            let #ctx_ident = __local_find_context!();
483            let #set_ident = #set;
484            #local_tokens
485            #attr_tokens
486            #entity_tokens
487            ::std::sync::Arc::new(
488                #base_path::query::intersectionconstraint::IntersectionConstraint::new(constraints)
489            )
490        }
491    };
492
493    Ok(output)
494}
495
496pub fn entity_impl(input: TokenStream2, base_path: &TokenStream2) -> syn::Result<TokenStream2> {
497    entity_impl_with(input, base_path, true)
498}
499
500/// Internal expansion used while constructing attribute descriptions. It emits
501/// the requested facts but does not recursively describe the metadata
502/// attributes used to express them.
503pub(crate) fn entity_impl_no_meta(
504    input: TokenStream2,
505    base_path: &TokenStream2,
506) -> syn::Result<TokenStream2> {
507    entity_impl_with(input, base_path, false)
508}
509
510fn entity_impl_with(
511    input: TokenStream2,
512    base_path: &TokenStream2,
513    emit_meta: bool,
514) -> syn::Result<TokenStream2> {
515    let wrapped = quote! { { #input } };
516
517    let Entity { id, attributes } = syn::parse2(wrapped)?;
518
519    let state_init = quote! {
520        #[allow(unused_mut)]
521        let mut __meta: #base_path::trible::TribleSet =
522            #base_path::trible::TribleSet::new();
523        let mut __blobs: #base_path::blob::MemoryBlobStore =
524            #base_path::blob::MemoryBlobStore::new();
525    };
526    let attr_count = attributes.len();
527
528    let mut attr_eval_tokens = TokenStream2::new();
529    let mut explicit_insert_tokens = TokenStream2::new();
530    let mut intrinsic_row_tokens = TokenStream2::new();
531    let mut extra_merge_tokens = TokenStream2::new();
532
533    for (i, attr) in attributes.into_iter().enumerate() {
534        let mode = attr.mode;
535        // entity! requires a concrete attribute constant — there's
536        // no meaningful way to "insert" a fact with a free
537        // attribute Id. Free attributes are a query-only construct.
538        let field_expr = match attr.name {
539            Inline::Expr(e) => e,
540            Inline::Var(id) => {
541                return Err(syn::Error::new_spanned(
542                    id,
543                    "variable attribute bindings (?ident) are not allowed in entity!; use a literal attribute reference here",
544                ));
545            }
546            Inline::LocalVar(id) => {
547                return Err(syn::Error::new_spanned(
548                    id,
549                    "local variable attribute bindings (_?ident) are not allowed in entity!; use a literal attribute reference here",
550                ));
551            }
552        };
553        let value_expr = match attr.value {
554            Inline::Expr(e) => e,
555            Inline::Var(id) => {
556                return Err(syn::Error::new_spanned(
557                    id,
558                    "variable bindings (?ident) are not allowed in entity!; use a literal expression here",
559                ));
560            }
561            Inline::LocalVar(id) => {
562                return Err(syn::Error::new_spanned(
563                    id,
564                    "local variable bindings (_?ident) are not allowed in entity!; use a literal expression here",
565                ));
566            }
567        };
568
569        let af_ident = format_ident!("__af{}", i, span = Span::mixed_site());
570        let val_ident = format_ident!("__val{}", i, span = Span::mixed_site());
571        let aid_ident = format_ident!("__a_id{}", i, span = Span::mixed_site());
572        let extra_ident = format_ident!("__extra{}", i, span = Span::mixed_site());
573
574        attr_eval_tokens.extend(quote! { let #af_ident = &#field_expr; });
575
576        // An attribute description is itself a Fragment. Its two fact sets are
577        // both descriptive relative to the entity being built, while its one
578        // blob store remains shared with the entity's content blobs.
579        let meta_tokens = if emit_meta {
580            quote! {
581                {
582                    let __attr_meta = #af_ident.meta();
583                    __meta.union(__attr_meta.facts().clone());
584                    __meta.union(__attr_meta.metafacts().clone());
585                    __blobs.union(__attr_meta.blobs().clone());
586                }
587            }
588        } else {
589            TokenStream2::new()
590        };
591
592        match mode {
593            AttributeMode::Required => {
594                attr_eval_tokens.extend(quote! {
595                    let (#val_ident, __maybe_blob) =
596                        #af_ident.encoded_from(#value_expr).into_parts();
597                    if let Some(__b) = __maybe_blob {
598                        __blobs.insert(__b);
599                    }
600                    #meta_tokens
601                });
602            }
603            AttributeMode::Optional => {
604                attr_eval_tokens.extend(quote! {
605                    let #val_ident = {
606                        let __opt: ::std::option::Option<_> = #value_expr;
607                        __opt.map(|__v| {
608                            let (__val, __maybe_blob) =
609                                #af_ident.encoded_from(__v).into_parts();
610                            if let Some(__b) = __maybe_blob {
611                                __blobs.insert(__b);
612                            }
613                            __val
614                        })
615                    };
616                    if #val_ident.is_some() {
617                        #meta_tokens
618                    }
619                });
620            }
621            AttributeMode::Repeated => {
622                attr_eval_tokens.extend(quote! {
623                    let (#val_ident, #extra_ident) = {
624                        let (__spread_iter, __spread_facts) =
625                            #base_path::trible::Spread::spread(#value_expr);
626                        let __vals = ::std::iter::IntoIterator::into_iter(__spread_iter)
627                            .map(|__v| {
628                                let (__val, __maybe_blob) =
629                                    #af_ident.encoded_from(__v).into_parts();
630                                if let Some(__b) = __maybe_blob {
631                                    __blobs.insert(__b);
632                                }
633                                __val
634                            })
635                            .collect::<::std::vec::Vec<_>>();
636                        (__vals, __spread_facts)
637                    };
638                    if !#val_ident.is_empty() {
639                        #meta_tokens
640                    }
641                });
642            }
643        }
644        attr_eval_tokens.extend(quote! { let #aid_ident = #af_ident.id(); });
645
646        match mode {
647            AttributeMode::Required => {
648                intrinsic_row_tokens.extend(quote! {
649                    __rows.push(#base_path::trible::IntrinsicEntityRow::new(
650                        #aid_ident,
651                        #val_ident.raw,
652                    ));
653                });
654                explicit_insert_tokens.extend(quote! {
655                    set.insert(&#base_path::trible::Trible::new(id_ref, &#aid_ident, &#val_ident));
656                });
657            }
658            AttributeMode::Optional => {
659                intrinsic_row_tokens.extend(quote! {
660                    if let Some(ref __v) = #val_ident {
661                        __rows.push(#base_path::trible::IntrinsicEntityRow::new(
662                            #aid_ident,
663                            __v.raw,
664                        ));
665                    }
666                });
667                explicit_insert_tokens.extend(quote! {
668                    if let Some(ref __v) = #val_ident {
669                        set.insert(&#base_path::trible::Trible::new(id_ref, &#aid_ident, __v));
670                    }
671                });
672            }
673            AttributeMode::Repeated => {
674                intrinsic_row_tokens.extend(quote! {
675                    for __v in #val_ident.iter() {
676                        __rows.push(#base_path::trible::IntrinsicEntityRow::new(
677                            #aid_ident,
678                            __v.raw,
679                        ));
680                    }
681                });
682                explicit_insert_tokens.extend(quote! {
683                    for __v in #val_ident.iter() {
684                        set.insert(&#base_path::trible::Trible::new(id_ref, &#aid_ident, __v));
685                    }
686                });
687                extra_merge_tokens.extend(quote! {
688                    let (_, __extra_facts, __extra_metafacts, __extra_blobs) =
689                        #extra_ident.into_parts();
690                    set += __extra_facts;
691                    __meta.union(__extra_metafacts);
692                    __blobs.union(__extra_blobs);
693                });
694            }
695        }
696    }
697
698    let entity_init: TokenStream2 = if let Some(val) = id {
699        match val {
700            Inline::Expr(expr) => quote! {
701                let id_tmp = #expr;
702                let id_ref: &#base_path::id::ExclusiveId = id_tmp.as_ref();
703                let root_id = id_ref.id;
704                let mut set = #base_path::trible::TribleSet::new();
705                #explicit_insert_tokens
706            },
707            Inline::Var(ident) => {
708                return Err(syn::Error::new_spanned(
709                    ident,
710                    "variable bindings (?ident) are not allowed in entity!; use a literal expression here",
711                ));
712            }
713            Inline::LocalVar(ident) => {
714                return Err(syn::Error::new_spanned(
715                    ident,
716                    "local variable bindings (_?ident) are not allowed in entity!; use a literal expression here",
717                ));
718            }
719        }
720    } else {
721        quote! {
722            let mut __rows: ::std::vec::Vec<#base_path::trible::IntrinsicEntityRow> =
723                ::std::vec::Vec::with_capacity(#attr_count);
724            #intrinsic_row_tokens
725            let (root_id, mut set) = #base_path::trible::build_intrinsic_entity(__rows);
726        }
727    };
728
729    let output = quote! {
730        {
731            #state_init
732            #attr_eval_tokens
733            #entity_init
734            #extra_merge_tokens
735            #base_path::trible::Fragment::rooted_from_parts(root_id, set, __meta, __blobs)
736        }
737    };
738
739    Ok(output)
740}
741
742struct PatternChangesInput {
743    curr: Expr,
744    changes: Expr,
745    pattern: Vec<Entity>,
746}
747
748impl Parse for PatternChangesInput {
749    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
750        let curr: Expr = input.parse()?;
751        input.parse::<Token![,]>()?;
752        let changes: Expr = input.parse()?;
753        input.parse::<Token![,]>()?;
754        let content;
755        bracketed!(content in input);
756
757        let mut pattern = Vec::new();
758        while !content.is_empty() {
759            pattern.push(content.parse()?);
760            if content.peek(Token![,]) {
761                content.parse::<Token![,]>()?;
762            }
763        }
764
765        Ok(PatternChangesInput {
766            curr,
767            changes,
768            pattern,
769        })
770    }
771}
772
773pub fn pattern_changes_impl(
774    input: TokenStream2,
775    base_path: &TokenStream2,
776) -> syn::Result<TokenStream2> {
777    use std::collections::HashMap;
778
779    let PatternChangesInput {
780        curr,
781        changes,
782        pattern,
783    } = syn::parse2(input)?;
784
785    let ctx_ident = format_ident!("__ctx", span = Span::mixed_site());
786    let curr_ident = format_ident!("__curr", span = Span::mixed_site());
787    let delta_ident = format_ident!("__delta", span = Span::mixed_site());
788
789    let mut attr_decl_tokens = TokenStream2::new();
790    let mut entity_decl_tokens = TokenStream2::new();
791    let mut entity_const_tokens = TokenStream2::new();
792    let mut value_decl_tokens = TokenStream2::new();
793
794    let mut triples = Vec::<TripleInfo>::new();
795
796    let mut attr_map: HashMap<String, (Ident, Ident)> = HashMap::new();
797    let mut attr_idx = 0usize;
798    let mut value_idx = 0usize;
799    let mut local_decl_tokens = TokenStream2::new();
800    let mut local_map: HashMap<String, Ident> = HashMap::new();
801    let mut local_idx = 0usize;
802
803    let mut get_local_var = |ident: &Ident| {
804        let key = format!("_?{}", ident);
805        local_map
806            .entry(key)
807            .or_insert_with(|| {
808                let ident = format_ident!("__local{}", local_idx, span = Span::mixed_site());
809                local_idx += 1;
810                local_decl_tokens.extend(quote! {
811                    let #ident = #ctx_ident.next_variable();
812                });
813                ident
814            })
815            .clone()
816    };
817
818    for (entity_idx, entity) in pattern.into_iter().enumerate() {
819        let e_ident = format_ident!("__e{}", entity_idx, span = Span::mixed_site());
820        let entity_var_key: Option<(bool, String)> = match entity.id {
821            Some(Inline::LocalVar(ref ident)) => Some((true, ident.to_string())),
822            Some(Inline::Var(ref ident)) => Some((false, ident.to_string())),
823            _ => None,
824        };
825        match entity.id {
826            Some(ref id_val) => match id_val {
827                Inline::Var(ref ident) => {
828                    entity_decl_tokens.extend(quote! { let #e_ident = #ident; });
829                }
830                Inline::LocalVar(ref ident) => {
831                    let local_ident = get_local_var(ident);
832                    entity_decl_tokens.extend(quote! { let #e_ident = #local_ident; });
833                }
834                Inline::Expr(ref id_expr) => {
835                    // Constant entity id: folded as a constant term.
836                    entity_decl_tokens.extend(quote! {
837                        let #e_ident: #base_path::inline::Inline<#base_path::inline::encodings::genid::GenId> = #base_path::inline::IntoInline::to_inline(#id_expr);
838                    });
839                }
840            },
841            None => {
842                entity_decl_tokens.extend(quote! {
843                    let #e_ident: #base_path::query::Variable<#base_path::inline::encodings::genid::GenId> = #ctx_ident.next_variable();
844                });
845            }
846        }
847
848        for Attribute {
849            name: attr_name,
850            mode,
851            value,
852        } in entity.attributes
853        {
854            // pattern_changes! currently requires concrete attribute
855            // constants — free-attribute (`?attr` / `_?attr`)
856            // support is a follow-up; the incremental delta walk
857            // needs more thought to remain spec-correct without a
858            // known schema for the value position.
859            let attr_expr = match attr_name {
860                Inline::Expr(e) => e,
861                Inline::Var(ident) => {
862                    return Err(syn::Error::new_spanned(
863                        ident,
864                        "free attribute (?ident) is not yet supported in pattern_changes!; use a concrete attribute constant",
865                    ));
866                }
867                Inline::LocalVar(ident) => {
868                    return Err(syn::Error::new_spanned(
869                        ident,
870                        "free attribute (_?ident) is not yet supported in pattern_changes!; use a concrete attribute constant",
871                    ));
872                }
873            };
874            if mode != AttributeMode::Required {
875                return Err(syn::Error::new_spanned(
876                    &attr_expr,
877                    "`?:` and `*:` are not supported in pattern_changes!; use `attr: value`",
878                ));
879            }
880            let key = attr_expr.to_token_stream().to_string();
881            let (a_ident, af_ident) = attr_map
882                .entry(key)
883                .or_insert_with(|| {
884                    let a_ident = format_ident!("__a{}", attr_idx, span = Span::mixed_site());
885                    let af_ident = format_ident!("__af{}", attr_idx, span = Span::mixed_site());
886                    attr_idx += 1;
887                    // Attribute constant: folded as a constant term —
888                    // no hidden variable, no ConstantConstraint.
889                    attr_decl_tokens.extend(quote! {
890                        let #af_ident = &#attr_expr;
891                        let #a_ident: #base_path::inline::Inline<#base_path::inline::encodings::genid::GenId> = #base_path::inline::IntoInline::to_inline(#af_ident.id());
892                    });
893                    (a_ident, af_ident)
894                })
895                .clone();
896
897            let v_ident = format_ident!("__v{}", value_idx, span = Span::mixed_site());
898            value_idx += 1;
899
900            let value_var_key: Option<(bool, String)> = match value {
901                Inline::Var(ref ident) => Some((false, ident.to_string())),
902                Inline::LocalVar(ref ident) => Some((true, ident.to_string())),
903                _ => None,
904            };
905            let self_ref = entity_var_key.is_some() && entity_var_key == value_var_key;
906
907            match value {
908                Inline::Expr(expr) => {
909                    // Literal value: folded as a constant term.
910                    value_decl_tokens.extend(quote! {
911                        let #v_ident = #af_ident.inline_from(#expr);
912                    });
913                }
914                Inline::Var(_) | Inline::LocalVar(_) if self_ref => {
915                    // Self-referencing: create fresh alias + equality
916                    let alias_ident =
917                        format_ident!("__alias{}", value_idx, span = Span::mixed_site());
918                    value_idx += 1;
919                    value_decl_tokens.extend(quote! {
920                        let #alias_ident: #base_path::query::Variable<#base_path::inline::encodings::genid::GenId> = #ctx_ident.next_variable();
921                        let #v_ident = #af_ident.as_variable(#alias_ident);
922                    });
923                    entity_const_tokens.extend(quote! {
924                        constraints.push(Box::new(#base_path::query::equalityconstraint::EqualityConstraint::new(#e_ident.index, #alias_ident.index)));
925                    });
926                }
927                Inline::Var(var_ident) => {
928                    value_decl_tokens.extend(quote! {
929                        let #v_ident = #af_ident.as_variable(#var_ident);
930                    });
931                }
932                Inline::LocalVar(ref var_ident) => {
933                    let local_ident = get_local_var(var_ident);
934                    value_decl_tokens.extend(quote! {
935                        let #v_ident = #af_ident.as_variable(#local_ident);
936                    });
937                }
938            }
939
940            triples.push(TripleInfo {
941                e_ident: e_ident.clone(),
942                a_ident: a_ident.clone(),
943                v_ident,
944            });
945        }
946    }
947
948    let mut case_exprs: Vec<TokenStream2> = Vec::new();
949    for delta_idx in 0..triples.len() {
950        let mut triple_tokens = TokenStream2::new();
951        for (
952            idx,
953            TripleInfo {
954                e_ident,
955                a_ident,
956                v_ident,
957            },
958        ) in triples.iter().enumerate()
959        {
960            let dataset = if idx == delta_idx {
961                &delta_ident
962            } else {
963                &curr_ident
964            };
965            triple_tokens.extend(quote! {
966                constraints.push(Box::new(#dataset.pattern(#e_ident, #a_ident, #v_ident)));
967            });
968        }
969
970        let case = quote! {
971            {
972                let mut constraints: ::std::vec::Vec<Box<dyn #base_path::query::Constraint + Send + Sync>> = ::std::vec::Vec::new();
973                #[allow(unused_imports)] use #base_path::query::TriblePattern;
974                #triple_tokens
975                ::std::sync::Arc::new(
976                    #base_path::query::intersectionconstraint::IntersectionConstraint::new(constraints)
977                )
978            }
979        };
980        case_exprs.push(case);
981    }
982
983    let union_expr = quote! {
984        ::std::sync::Arc::new(
985            #base_path::query::unionconstraint::UnionConstraint::new(vec![
986                #(Box::new(#case_exprs) as Box<dyn #base_path::query::Constraint + Send + Sync>),*
987            ])
988        )
989    };
990
991    let output = quote! {
992        {
993            let #ctx_ident = __local_find_context!();
994            let #curr_ident = #curr;
995            let #delta_ident = #changes;
996            #attr_decl_tokens
997            #local_decl_tokens
998            #entity_decl_tokens
999            #value_decl_tokens
1000            let mut constraints: ::std::vec::Vec<Box<dyn #base_path::query::Constraint + Send + Sync>> = ::std::vec::Vec::new();
1001            #[allow(unused_imports)] use #base_path::query::TriblePattern;
1002            #entity_const_tokens
1003            constraints.push(Box::new(#union_expr));
1004            ::std::sync::Arc::new(
1005                #base_path::query::intersectionconstraint::IntersectionConstraint::new(constraints)
1006            )
1007        }
1008    };
1009
1010    Ok(output)
1011}
1012
1013struct TripleInfo {
1014    e_ident: Ident,
1015    a_ident: Ident,
1016    v_ident: Ident,
1017}