vyre_foundation/ir_inner/model/expr.rs
1// Expression nodes - produce values.
2//
3// Every expression evaluates to a typed value. Expressions are pure:
4// they read state but do not modify it.
5
6use crate::ir_inner::model::spec_types::{DataType, SubgroupReduceOp};
7use rustc_hash::FxHasher;
8use std::borrow::Borrow;
9use std::fmt;
10use std::hash::{Hash, Hasher};
11use std::ops::Deref;
12use std::sync::Arc;
13
14/// Reference to the generator/macro that produced an AST region.
15/// Used for source-mapping and DWARF-like debugging context.
16#[derive(Debug, Clone, PartialEq, Eq, Hash)]
17pub struct GeneratorRef {
18 /// The name of the generator (e.g., `vyre-nn::flash_attention`).
19 pub name: String,
20}
21
22/// Interned identifier used by expression nodes.
23///
24/// `Ident` is cheap to clone and keeps expression trees from repeatedly
25/// allocating owned `String` values for the same variable or buffer names.
26#[derive(Clone, Eq, PartialEq)]
27pub struct Ident {
28 text: Arc<str>,
29 hash: u64,
30}
31
32impl Ident {
33 #[inline]
34 fn prehash(text: &str) -> u64 {
35 let mut hasher = FxHasher::default();
36 text.hash(&mut hasher);
37 hasher.finish()
38 }
39
40 #[must_use]
41 #[inline]
42 /// Construct an identifier from shared text while caching its hash once.
43 pub fn new(text: Arc<str>) -> Self {
44 let hash = Self::prehash(&text);
45 Self { text, hash }
46 }
47
48 /// Clone the underlying interned string handle without copying UTF-8 bytes.
49 #[must_use]
50 #[inline]
51 pub fn shared_text(&self) -> Arc<str> {
52 Arc::clone(&self.text)
53 }
54
55 /// Return another identifier handle to the same interned text without
56 /// reallocating text or recomputing the cached hash.
57 #[must_use]
58 #[inline]
59 pub fn duplicate_handle(&self) -> Self {
60 Self {
61 text: Arc::clone(&self.text),
62 hash: self.hash,
63 }
64 }
65
66 /// Return the identifier text.
67 #[must_use]
68 #[inline]
69 pub fn as_str(&self) -> &str {
70 &self.text
71 }
72
73 /// Return the cached hash used by hash-map/set lookups.
74 #[must_use]
75 #[inline]
76 pub fn cached_hash(&self) -> u64 {
77 self.hash
78 }
79}
80
81impl From<&str> for Ident {
82 #[inline]
83 fn from(value: &str) -> Self {
84 Self::new(Arc::from(value))
85 }
86}
87
88impl From<String> for Ident {
89 #[inline]
90 fn from(value: String) -> Self {
91 Self::new(Arc::from(value))
92 }
93}
94
95impl From<Arc<str>> for Ident {
96 #[inline]
97 fn from(value: Arc<str>) -> Self {
98 Self::new(value)
99 }
100}
101
102impl From<&String> for Ident {
103 #[inline]
104 fn from(value: &String) -> Self {
105 Self::from(value.as_str())
106 }
107}
108
109impl From<&Ident> for Ident {
110 #[inline]
111 fn from(value: &Ident) -> Self {
112 value.clone()
113 }
114}
115
116impl fmt::Debug for Ident {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 f.debug_tuple("Ident").field(&self.as_str()).finish()
119 }
120}
121
122impl Hash for Ident {
123 /// Audit P-IDENT-BORROW (2026-04-29): hash via the underlying str so the
124 /// `Hash` impl matches the `Borrow<str>` impl, preserving the
125 /// `HashMap::get<Q: Borrow<K> + Hash + Eq>` invariant. The
126 /// pre-fix `state.write_u64(self.hash)` produced a different u64 than
127 /// `<str as Hash>::hash` for the same hasher (which writes bytes + a
128 /// length terminator), so any `FxHashMap<Ident, V>::get(&str)` lookup
129 /// silently missed the inserted entry. Callers that want the cached
130 /// `FxHash` for a fast equality-check key call [`Ident::cached_hash`]
131 /// directly.
132 #[inline]
133 fn hash<H: Hasher>(&self, state: &mut H) {
134 self.text.hash(state);
135 }
136}
137
138impl Deref for Ident {
139 type Target = str;
140
141 #[inline]
142 fn deref(&self) -> &Self::Target {
143 self.as_str()
144 }
145}
146
147impl AsRef<str> for Ident {
148 #[inline]
149 fn as_ref(&self) -> &str {
150 self.as_str()
151 }
152}
153
154impl Borrow<str> for Ident {
155 #[inline]
156 fn borrow(&self) -> &str {
157 self.as_str()
158 }
159}
160
161impl fmt::Display for Ident {
162 #[inline]
163 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164 f.write_str(self.as_str())
165 }
166}
167
168impl PartialEq<str> for Ident {
169 #[inline]
170 fn eq(&self, other: &str) -> bool {
171 self.as_str() == other
172 }
173}
174
175impl PartialEq<&str> for Ident {
176 #[inline]
177 fn eq(&self, other: &&str) -> bool {
178 self.as_str() == *other
179 }
180}
181
182impl PartialOrd for Ident {
183 #[inline]
184 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
185 Some(self.cmp(other))
186 }
187}
188
189impl Ord for Ident {
190 #[inline]
191 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
192 self.as_str().cmp(other.as_str())
193 }
194}
195
196/// An expression that produces a value.
197///
198/// # Examples
199///
200/// ```
201/// use vyre::ir::Expr;
202///
203/// let lit = Expr::u32(42);
204/// let var = Expr::var("x");
205/// let add = Expr::add(lit, var);
206/// ```
207pub use crate::ir_inner::model::generated::Expr;
208
209/// Public contract for downstream expression extension nodes.
210///
211/// Extension nodes are intentionally opaque to core. A downstream crate owns
212/// the semantic payload and provides the stable metadata core needs for
213/// validation, debug output, equality, and CSE identity. Backends that
214/// understand the extension can downcast through their own wrapper type before
215/// constructing target code; backends that do not understand it must reject it
216/// with an actionable error.
217pub trait ExprNode: fmt::Debug + Send + Sync + 'static {
218 /// Stable extension namespace, for example `my_backend.tensor.shuffle`.
219 fn extension_kind(&self) -> &'static str;
220
221 /// Human-readable identity used in diagnostics and debug logs.
222 fn debug_identity(&self) -> &str;
223
224 /// Static result type produced by this expression.
225 fn result_type(&self) -> Option<DataType>;
226
227 /// Whether CSE may treat this extension as a pure, repeatable expression.
228 fn cse_safe(&self) -> bool;
229
230 /// Stable, content-addressed identity for equality and optimizer keys.
231 fn stable_fingerprint(&self) -> [u8; 32];
232
233 /// Validate extension-local invariants.
234 ///
235 /// # Errors
236 ///
237 /// The returned error must explain the bad invariant and include `Fix:`.
238 fn validate_extension(&self) -> Result<(), String>;
239
240 /// Downcast to Any to allow backend-specific dispatch from opaque payloads.
241 fn as_any(&self) -> &dyn std::any::Any;
242
243 /// Serialize the extension payload into stable bytes used by the wire
244 /// encoder's `Expr::Opaque` path (tag `0x80`). Default: empty payload -
245 /// suitable for extensions that carry no state beyond their type
246 /// identity. Extensions with state must override this to emit the exact
247 /// bytes `wire_payload`'s matching `OpaqueExprResolver` will consume.
248 ///
249 /// The payload contract is endian-fixed: any numeric field wider than
250 /// one byte MUST be written with `to_le_bytes`, and the matching decoder
251 /// MUST reconstruct it with `from_le_bytes`. Host-endian encodings such as
252 /// `to_ne_bytes` are forbidden because the wire format must stay
253 /// byte-identical across architectures.
254 ///
255 /// Extension authors should use [`crate::opaque_payload::endian::LeBytesWriter`] when
256 /// building payloads because it makes the required endianness explicit in the type.
257 ///
258 /// Literal extensions that encode regex payloads must also canonicalize
259 /// inline flag prefixes before emitting bytes. For example, `(?mi)` and
260 /// `(?im)` are the same semantic payload and MUST serialize to the same
261 /// flag ordering.
262 fn wire_payload(&self) -> Vec<u8> {
263 Vec::new()
264 }
265}
266
267impl Expr {
268 /// Load from buffer at index.
269 ///
270 /// # Examples
271 ///
272 /// ```
273 /// use vyre::ir::Expr;
274 /// let _ = Expr::load("a", Expr::u32(0));
275 /// ```
276 #[must_use]
277 #[inline]
278 pub fn load(buffer: impl Into<Ident>, index: Self) -> Self {
279 Self::Load {
280 buffer: buffer.into(),
281 index: Box::new(index),
282 }
283 }
284
285 /// Buffer element count.
286 ///
287 /// # Examples
288 ///
289 /// ```
290 /// use vyre::ir::Expr;
291 /// let _ = Expr::buf_len("a");
292 /// ```
293 #[must_use]
294 #[inline]
295 pub fn buf_len(buffer: impl Into<Ident>) -> Self {
296 Self::BufLen {
297 buffer: buffer.into(),
298 }
299 }
300
301 /// Name a whole buffer as an argument to a composite op.
302 ///
303 /// This is not a value: it has no type and only call-argument position
304 /// accepts it. Inlining rebinds the callee's matching parameter onto
305 /// this buffer, so a callee that reads `table[i]` ends up reading the
306 /// caller's buffer at the same index. To read one element instead, use
307 /// [`Expr::load`].
308 ///
309 /// ```
310 /// use vyre::ir::Expr;
311 /// let _ = Expr::call("dialect::lookup", vec![Expr::buffer_ref("table"), Expr::u32(3)]);
312 /// ```
313 #[must_use]
314 #[inline]
315 pub fn buffer_ref(buffer: impl Into<Ident>) -> Self {
316 Self::BufferRef {
317 buffer: buffer.into(),
318 }
319 }
320
321 /// `global_invocation_id.x`
322 #[must_use]
323 #[inline]
324 pub fn gid_x() -> Self {
325 Self::InvocationId { axis: 0 }
326 }
327
328 /// `global_invocation_id.y`
329 #[must_use]
330 #[inline]
331 pub fn gid_y() -> Self {
332 Self::InvocationId { axis: 1 }
333 }
334
335 /// `global_invocation_id.z`
336 #[must_use]
337 #[inline]
338 pub fn gid_z() -> Self {
339 Self::InvocationId { axis: 2 }
340 }
341
342 /// `workgroup_id.x`
343 #[must_use]
344 #[inline]
345 pub fn workgroup_x() -> Self {
346 Self::WorkgroupId { axis: 0 }
347 }
348
349 /// `workgroup_id.y`
350 #[must_use]
351 #[inline]
352 pub fn workgroup_y() -> Self {
353 Self::WorkgroupId { axis: 1 }
354 }
355
356 /// `workgroup_id.z`
357 #[must_use]
358 #[inline]
359 pub fn workgroup_z() -> Self {
360 Self::WorkgroupId { axis: 2 }
361 }
362
363 /// Predicate `workgroup_id.x == 0`: the canonical "first parallel region only" guard.
364 ///
365 /// Single-workgroup kernels (scalar reductions, workgroup-local tree reductions, and any
366 /// kernel meant to run in exactly one parallel region) gate their body on this so a dispatch
367 /// of more than one workgroup leaves the extra regions as no-ops instead of double-counting or
368 /// racing on the shared output. Prefer this over re-spelling `eq(WorkgroupId{axis:0}, 0)`
369 /// inline so the "first workgroup" contract has one owner.
370 #[must_use]
371 #[inline]
372 pub fn is_first_workgroup() -> Self {
373 Self::eq(Self::WorkgroupId { axis: 0 }, Self::u32(0))
374 }
375
376 /// `local_invocation_id.x`
377 #[must_use]
378 #[inline]
379 pub fn local_x() -> Self {
380 Self::LocalId { axis: 0 }
381 }
382
383 /// `subgroup_invocation_id` (lane index within subgroup).
384 #[must_use]
385 #[inline]
386 pub fn subgroup_local_id() -> Self {
387 Self::SubgroupLocalId
388 }
389
390 /// `subgroup_size` (number of lanes per subgroup).
391 #[must_use]
392 #[inline]
393 pub fn subgroup_size() -> Self {
394 Self::SubgroupSize
395 }
396
397 /// `local_invocation_id.y`
398 #[must_use]
399 #[inline]
400 pub fn local_y() -> Self {
401 Self::LocalId { axis: 1 }
402 }
403
404 /// `local_invocation_id.z`
405 #[must_use]
406 #[inline]
407 pub fn local_z() -> Self {
408 Self::LocalId { axis: 2 }
409 }
410
411 /// Substrate-neutral alias for [`workgroup_x`](Self::workgroup_x).
412 ///
413 /// "Parallel region" is the vocabulary used in the public `vyre` facade.
414 /// Concrete drivers translate this concept into target vocabulary at the
415 /// lowering boundary.
416 #[must_use]
417 #[inline]
418 pub fn parallel_region_x() -> Self {
419 Self::WorkgroupId { axis: 0 }
420 }
421
422 /// Substrate-neutral alias for [`workgroup_y`](Self::workgroup_y).
423 #[must_use]
424 #[inline]
425 pub fn parallel_region_y() -> Self {
426 Self::WorkgroupId { axis: 1 }
427 }
428
429 /// Substrate-neutral alias for [`workgroup_z`](Self::workgroup_z).
430 #[must_use]
431 #[inline]
432 pub fn parallel_region_z() -> Self {
433 Self::WorkgroupId { axis: 2 }
434 }
435
436 /// Substrate-neutral alias for [`local_x`](Self::local_x).
437 #[must_use]
438 #[inline]
439 pub fn invocation_local_x() -> Self {
440 Self::LocalId { axis: 0 }
441 }
442
443 /// Substrate-neutral alias for [`local_y`](Self::local_y).
444 #[must_use]
445 #[inline]
446 pub fn invocation_local_y() -> Self {
447 Self::LocalId { axis: 1 }
448 }
449
450 /// Substrate-neutral alias for [`local_z`](Self::local_z).
451 #[must_use]
452 #[inline]
453 pub fn invocation_local_z() -> Self {
454 Self::LocalId { axis: 2 }
455 }
456
457 /// Conditional select.
458 #[must_use]
459 #[inline]
460 pub fn select(cond: Self, true_val: Self, false_val: Self) -> Self {
461 Self::Select {
462 cond: Box::new(cond),
463 true_val: Box::new(true_val),
464 false_val: Box::new(false_val),
465 }
466 }
467
468 /// Subgroup reduction across the active subgroup with the given operator.
469 #[must_use]
470 #[inline]
471 pub fn subgroup_reduce(op: SubgroupReduceOp, value: Self) -> Self {
472 Self::SubgroupReduce {
473 op,
474 value: Box::new(value),
475 }
476 }
477
478 /// Subgroup sum reduction across the active subgroup.
479 #[must_use]
480 #[inline]
481 pub fn subgroup_add(value: Self) -> Self {
482 Self::subgroup_reduce(SubgroupReduceOp::Add, value)
483 }
484
485 /// Subgroup product reduction across the active subgroup.
486 #[must_use]
487 #[inline]
488 pub fn subgroup_mul(value: Self) -> Self {
489 Self::subgroup_reduce(SubgroupReduceOp::Mul, value)
490 }
491
492 /// Subgroup minimum reduction across the active subgroup.
493 #[must_use]
494 #[inline]
495 pub fn subgroup_min(value: Self) -> Self {
496 Self::subgroup_reduce(SubgroupReduceOp::Min, value)
497 }
498
499 /// Subgroup maximum reduction across the active subgroup.
500 #[must_use]
501 #[inline]
502 pub fn subgroup_max(value: Self) -> Self {
503 Self::subgroup_reduce(SubgroupReduceOp::Max, value)
504 }
505
506 /// Subgroup bitwise-AND reduction across the active subgroup.
507 #[must_use]
508 #[inline]
509 pub fn subgroup_and(value: Self) -> Self {
510 Self::subgroup_reduce(SubgroupReduceOp::And, value)
511 }
512
513 /// Subgroup bitwise-OR reduction across the active subgroup.
514 #[must_use]
515 #[inline]
516 pub fn subgroup_or(value: Self) -> Self {
517 Self::subgroup_reduce(SubgroupReduceOp::Or, value)
518 }
519
520 /// Subgroup bitwise-XOR reduction across the active subgroup.
521 #[must_use]
522 #[inline]
523 pub fn subgroup_xor(value: Self) -> Self {
524 Self::subgroup_reduce(SubgroupReduceOp::Xor, value)
525 }
526
527 /// Subgroup shuffle: broadcast `value` from the given lane id to
528 /// every active lane in the subgroup.
529 #[must_use]
530 #[inline]
531 pub fn subgroup_shuffle(value: Self, lane: Self) -> Self {
532 Self::SubgroupShuffle {
533 value: Box::new(value),
534 lane: Box::new(lane),
535 }
536 }
537
538 /// Subgroup ballot: gather the boolean predicate `cond` across
539 /// the active subgroup into a single bitmask.
540 #[must_use]
541 #[inline]
542 pub fn subgroup_ballot(cond: Self) -> Self {
543 Self::SubgroupBallot {
544 cond: Box::new(cond),
545 }
546 }
547
548 /// Named variable reference.
549 #[must_use]
550 #[inline]
551 pub fn var(name: impl Into<Ident>) -> Self {
552 Self::Var(name.into())
553 }
554
555 /// Unsigned 32-bit literal.
556 #[must_use]
557 #[inline]
558 pub fn u32(value: u32) -> Self {
559 Self::LitU32(value)
560 }
561
562 /// Signed 32-bit literal.
563 #[must_use]
564 #[inline]
565 pub fn i32(value: i32) -> Self {
566 Self::LitI32(value)
567 }
568
569 /// 32-bit floating-point literal.
570 #[must_use]
571 #[inline]
572 pub fn f32(value: f32) -> Self {
573 Self::LitF32(value)
574 }
575
576 /// Boolean literal.
577 #[must_use]
578 #[inline]
579 pub fn bool(value: bool) -> Self {
580 Self::LitBool(value)
581 }
582
583 /// Operation call by stable operation ID.
584 #[must_use]
585 #[inline]
586 pub fn call(op_id: impl Into<Ident>, args: Vec<Self>) -> Self {
587 Self::Call {
588 op_id: op_id.into(),
589 args,
590 }
591 }
592
593 /// Fused multiply-add `a * b + c` (f32).
594 #[must_use]
595 #[inline]
596 pub fn fma(a: Self, b: Self, c: Self) -> Self {
597 Self::Fma {
598 a: Box::new(a),
599 b: Box::new(b),
600 c: Box::new(c),
601 }
602 }
603
604 /// Cast a value to `target`.
605 #[must_use]
606 #[inline]
607 pub fn cast(target: DataType, value: Self) -> Self {
608 Self::Cast {
609 target,
610 value: Box::new(value),
611 }
612 }
613
614 /// Wrap a downstream extension expression node.
615 #[must_use]
616 #[inline]
617 pub fn opaque(node: impl ExprNode) -> Self {
618 Self::Opaque(Arc::new(node))
619 }
620
621 /// Wrap a shared downstream extension expression node.
622 #[must_use]
623 #[inline]
624 pub fn opaque_arc(node: Arc<dyn ExprNode>) -> Self {
625 Self::Opaque(node)
626 }
627}
628
629mod atomics;
630mod builders;
631
632#[cfg(test)]
633mod tests {
634 use super::Expr;
635
636 #[test]
637 fn expr_size_is_bounded() {
638 let size = std::mem::size_of::<Expr>();
639 assert!(
640 size <= 128,
641 "Expr grew to {size} bytes. Fix: box the largest variant before adding more fields."
642 );
643 }
644}