From cac4f8c00a43a4fec4dc62f40bf129d387812555 Mon Sep 17 00:00:00 2001 From: Eduard-Mihai Burtescu Date: Thu, 8 Oct 2026 10:24:35 +0300 Subject: [PATCH 1/4] spv/print: generalize `operand_from_imms` to `operand` that can also take IDs. --- src/print/mod.rs | 6 +++--- src/spv/print.rs | 12 ++++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/print/mod.rs b/src/print/mod.rs index 5c0d1d05..f7390074 100644 --- a/src/print/mod.rs +++ b/src/print/mod.rs @@ -1874,7 +1874,7 @@ impl Printer<'_> { &self, imms: impl IntoIterator, ) -> pretty::Fragment { - self.pretty_spv_print_tokens_for_operand(spv::print::operand_from_imms(imms)) + self.pretty_spv_print_tokens_for_operand(spv::print::operand(imms, [])) } /// Pretty-print a single SPIR-V (short) immediate (e.g. an enumerand). @@ -2419,7 +2419,7 @@ impl Print for spv::Dialect { // HACK(eddyb) construct a custom `spv.Capability.{A,B,C}`. let capability_namespace_prefix = printer .pretty_spv_print_tokens_for_operand({ - let mut tokens = spv::print::operand_from_imms(cap_imms(0)); + let mut tokens = spv::print::operand(cap_imms(0), []); assert!(matches!( tokens.tokens.pop(), Some(spv::print::Token::EnumerandName(_)) @@ -2429,7 +2429,7 @@ impl Print for spv::Dialect { let mut cap_names = capabilities.iter().map(|&cap| { printer.pretty_spv_print_tokens_for_operand({ - let mut tokens = spv::print::operand_from_imms(cap_imms(cap)); + let mut tokens = spv::print::operand(cap_imms(cap), []); tokens.tokens.drain(..tokens.tokens.len() - 1); assert!(matches!( tokens.tokens[..], diff --git a/src/spv/print.rs b/src/spv/print.rs index 1c1c597a..cd80ccdd 100644 --- a/src/spv/print.rs +++ b/src/spv/print.rs @@ -255,12 +255,15 @@ impl, ID, IDS: Iterator> OperandPrint } } -/// Print a single SPIR-V operand from only immediates, potentially composed of -/// an enumerand with parameters (which consumes more immediates). -pub fn operand_from_imms(imms: impl IntoIterator) -> TokensForOperand { +/// Print a single SPIR-V operand from `imms` and `ids`, described by `imms[0]`, +/// but which may consume more immediates/IDs (e.g. for enumerand parameters). +pub fn operand( + imms: impl IntoIterator, + ids: impl IntoIterator, +) -> TokensForOperand { let mut printer = OperandPrinter { imms: imms.into_iter().peekable(), - ids: iter::empty().peekable(), + ids: ids.into_iter().peekable(), out: TokensForOperand::default(), }; let &kind = match printer.imms.peek().unwrap() { @@ -269,6 +272,7 @@ pub fn operand_from_imms(imms: impl IntoIterator) -> TokensF }; printer.operand("", kind); assert!(printer.imms.next().is_none()); + assert!(printer.ids.next().is_none()); printer.out } From e9fdfd2271a9dd1f269a3c3ecf5372dcb8cec175 Mon Sep 17 00:00:00 2001 From: Eduard-Mihai Burtescu Date: Thu, 8 Oct 2026 17:01:02 +0300 Subject: [PATCH 2/4] Support IDs (`Const`s) in `Attr::SpvAnnotation` (for e.g. `OpDecorateId`). --- src/lib.rs | 15 ++++++- src/print/mod.rs | 110 ++++++++++++++++++++++++++++++++------------- src/qptr/layout.rs | 18 ++++++-- src/qptr/lift.rs | 55 +++++++++++++---------- src/spv/lift.rs | 30 +++++++++---- src/spv/lower.rs | 83 +++++++++++++++++++++++++++------- src/spv/spec.rs | 3 +- src/transform.rs | 26 +++++++++-- src/visit.rs | 12 ++++- 9 files changed, 265 insertions(+), 87 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f6baca75..872000f5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -398,7 +398,20 @@ pub enum Attr { #[from] QPtr(qptr::QPtrAttr), - SpvAnnotation(spv::Inst), + SpvAnnotation { + spv_inst: spv::Inst, + + /// If present, each "instance" represents a different use of `spv_inst`, + /// grouped together only because the non-ID operands (enumerand/literals) + /// inside `spv::Inst` are not enough to uniquely identify an annotation + /// (i.e. at least some of the `Const` IDs are semantically part of the + /// "key" side `Attr`, which should normally be outside the `OrdAssertEq`). + // + // HACK(eddyb) this is enough of a complication to furthger call into + // question the use of ordered sets for `AttrSet`s, in the first place. + #[allow(clippy::type_complexity)] + per_instance_const_inputs: Option>>>>, + }, /// Some SPIR-V instructions, like `OpFunction`, take a bitflags operand /// that is effectively an optimization over using `OpDecorate`. diff --git a/src/print/mod.rs b/src/print/mod.rs index f7390074..c852da38 100644 --- a/src/print/mod.rs +++ b/src/print/mod.rs @@ -483,7 +483,12 @@ impl<'a> Visitor<'a> for Plan<'a> { .attrs .iter() .filter_map(|attr| match attr { - Attr::SpvAnnotation(spv_inst) if spv_inst.opcode == wk.OpName => Some(spv_inst), + Attr::SpvAnnotation { spv_inst, per_instance_const_inputs } + if spv_inst.opcode == wk.OpName => + { + assert!(per_instance_const_inputs.is_none()); + Some(spv_inst) + } _ => None, }) .peekable(); @@ -2703,7 +2708,9 @@ impl Print for AttrSet { attrs .iter() .filter(|attr| match attr { - Attr::SpvAnnotation(spv_inst) => Some(spv_inst) != spv_name_to_hide, + Attr::SpvAnnotation { spv_inst, per_instance_const_inputs: None } => { + Some(spv_inst) != spv_name_to_hide + } _ => true, }) .map(|attr| attr.print(printer)) @@ -2781,7 +2788,14 @@ impl DbgScope { impl Print for Attr { type Output = pretty::Fragment; fn print(&self, printer: &Printer<'_>) -> pretty::Fragment { - let non_comment_attr = match self { + let mk_non_comment_attr = |attr| { + pretty::Fragment::new([ + printer.attr_style().apply("#[").into(), + attr, + printer.attr_style().apply("]").into(), + ]) + }; + match self { &Attr::DbgSrcLoc(OrdAssertEq(dbg_src_loc)) => { let mut comment = SmallVec::<[_; 4]>::new(); @@ -2869,11 +2883,11 @@ impl Print for Attr { } } - return comment; + comment } Attr::Diagnostics(diags) => { - return pretty::Fragment::new( + pretty::Fragment::new( diags .0 .iter() @@ -2947,7 +2961,7 @@ impl Print for Attr { ]) }) .intersperse(pretty::Node::ForceLineSeparation.into()), - ); + ) } Attr::QPtr(attr) => { @@ -2983,41 +2997,77 @@ impl Print for Attr { ("usage", pretty::join_comma_sep("(", [usage.0.print(printer)], ")")) } }; - pretty::Fragment::new([ + mk_non_comment_attr(pretty::Fragment::new([ printer .demote_style_for_namespace_prefix(printer.attr_style()) .apply("qptr.") .into(), printer.attr_style().apply(name).into(), params_inputs, - ]) + ])) } - Attr::SpvAnnotation(spv::Inst { opcode, imms }) => { + Attr::SpvAnnotation { + spv_inst: spv::Inst { opcode, imms }, + per_instance_const_inputs, + } => { let wk = &spv::spec::Spec::get().well_known; - // HACK(eddyb) `#[spv.OpDecorate(...)]` is redundant (with its operand). - if [wk.OpDecorate, wk.OpDecorateString, wk.OpExecutionMode].contains(opcode) { - printer.pretty_spv_operand_from_imms(imms.iter().copied()) - } else if *opcode == wk.OpName { - // HACK(eddyb) unlike `OpDecorate`, we can't just omit `OpName`, - // but pretending it's a SPIR-T-specific `#[name = "..."]` - // attribute should be good enough for now. - pretty::Fragment::new([ - printer.attr_style().apply("name = ").into(), - printer.pretty_spv_operand_from_imms(imms.iter().copied()), - ]) - } else { - printer.pretty_spv_inst(printer.attr_style(), *opcode, imms, [None]) - } + let empty_const_inputs = SmallVec::new(); + let instances = + per_instance_const_inputs + .as_ref() + .map_or(std::slice::from_ref(&empty_const_inputs), |instances| { + &instances.0[..] + }); + + pretty::Fragment::new( + instances + .iter() + .map(|const_inputs| { + // HACK(eddyb) `#[spv.OpDecorate(...)]` is redundant (with its operand). + if [ + wk.OpDecorate, + wk.OpDecorateId, + wk.OpDecorateString, + wk.OpExecutionMode, + wk.OpExecutionModeId, + ] + .contains(opcode) + { + printer.pretty_spv_print_tokens_for_operand(spv::print::operand( + imms.iter().copied(), + const_inputs.iter().map(|ct| ct.print(printer).into()), + )) + } else if *opcode == wk.OpName { + assert_eq!(const_inputs.len(), 0); + + // HACK(eddyb) unlike `OpDecorate`, we can't just omit `OpName`, + // but pretending it's a SPIR-T-specific `#[name = "..."]` + // attribute should be good enough for now. + pretty::Fragment::new([ + printer.attr_style().apply("name = ").into(), + printer.pretty_spv_operand_from_imms(imms.iter().copied()), + ]) + } else { + printer.pretty_spv_inst( + printer.attr_style(), + *opcode, + imms, + [None].into_iter().chain( + const_inputs.iter().map(|ct| ct.print(printer)).map(Some), + ), + ) + } + }) + .map(mk_non_comment_attr) + .intersperse_with(|| pretty::Node::ForceLineSeparation.into()), + ) } - &Attr::SpvBitflagsOperand(imm) => printer.pretty_spv_operand_from_imms([imm]), - }; - pretty::Fragment::new([ - printer.attr_style().apply("#[").into(), - non_comment_attr, - printer.attr_style().apply("]").into(), - ]) + &Attr::SpvBitflagsOperand(imm) => { + mk_non_comment_attr(printer.pretty_spv_operand_from_imms([imm])) + } + } } } diff --git a/src/qptr/layout.rs b/src/qptr/layout.rs index cbc407b1..8837eb60 100644 --- a/src/qptr/layout.rs +++ b/src/qptr/layout.rs @@ -399,11 +399,14 @@ impl<'a> LayoutCache<'a> { let mut stride_decoration = None; for attr in &cx[ty_def.attrs].attrs { match attr { - Attr::SpvAnnotation(attr_spv_inst) + Attr::SpvAnnotation { spv_inst: attr_spv_inst, per_instance_const_inputs } if attr_spv_inst.opcode == wk.OpDecorate && attr_spv_inst.imms[0] == spv::Imm::Short(wk.Decoration, wk.ArrayStride) => { + // FIXME(eddyb) support `ArrayStrideId`. + assert!(per_instance_const_inputs.is_none()); + stride_decoration = Some(match attr_spv_inst.imms[1] { spv::Imm::Short(_, x) => x, _ => unreachable!(), @@ -445,20 +448,25 @@ impl<'a> LayoutCache<'a> { let mut field_offsets: SmallVec<[_; 4]> = SmallVec::with_capacity(field_layouts.len()); for attr in &cx[ty_def.attrs].attrs { match attr { - Attr::SpvAnnotation(attr_spv_inst) + Attr::SpvAnnotation { spv_inst: attr_spv_inst, per_instance_const_inputs } if attr_spv_inst.opcode == wk.OpMemberDecorate && attr_spv_inst.imms[1] == spv::Imm::Short(wk.Decoration, wk.RowMajor) => { + assert!(per_instance_const_inputs.is_none()); + return Err(LayoutError(Diag::bug([ "`RowMajor` matrix types unsupported".into(), ]))); } - Attr::SpvAnnotation(attr_spv_inst) + Attr::SpvAnnotation { spv_inst: attr_spv_inst, per_instance_const_inputs } if attr_spv_inst.opcode == wk.OpMemberDecorate && attr_spv_inst.imms[1] == spv::Imm::Short(wk.Decoration, wk.Offset) => { + // FIXME(eddyb) support `OffsetId`. + assert!(per_instance_const_inputs.is_none()); + let (field_idx, field_offset) = match attr_spv_inst.imms[..] { [spv::Imm::Short(_, idx), _, spv::Imm::Short(_, offset)] => { (idx, offset) @@ -597,11 +605,13 @@ impl<'a> LayoutCache<'a> { let mut is_interface_block = false; for attr in &cx[ty_def.attrs].attrs { match attr { - Attr::SpvAnnotation(attr_spv_inst) + Attr::SpvAnnotation { spv_inst: attr_spv_inst, per_instance_const_inputs } if attr_spv_inst.opcode == wk.OpDecorate && attr_spv_inst.imms[0] == spv::Imm::Short(wk.Decoration, wk.Block) => { + assert!(per_instance_const_inputs.is_none()); + is_interface_block = true; break; } diff --git a/src/qptr/lift.rs b/src/qptr/lift.rs index c4af4e2f..b815f43a 100644 --- a/src/qptr/lift.rs +++ b/src/qptr/lift.rs @@ -203,10 +203,13 @@ impl<'a> LiftToSpvPtrs<'a> { match usage { &QPtrUsage::Handles(shapes::Handle::Opaque(ty)) => Ok(ty), QPtrUsage::Handles(shapes::Handle::Buffer(_, data_usage)) => { - let attr_spv_decorate_block = Attr::SpvAnnotation(spv::Inst { - opcode: wk.OpDecorate, - imms: [spv::Imm::Short(wk.Decoration, wk.Block)].into_iter().collect(), - }); + let attr_spv_decorate_block = Attr::SpvAnnotation { + spv_inst: spv::Inst { + opcode: wk.OpDecorate, + imms: [spv::Imm::Short(wk.Decoration, wk.Block)].into_iter().collect(), + }, + per_instance_const_inputs: None, + }; match &data_usage.kind { QPtrMemUsageKind::Unused => { self.spv_op_type_struct([], [attr_spv_decorate_block]) @@ -270,15 +273,18 @@ impl<'a> LiftToSpvPtrs<'a> { let stride_attrs = stride.map(|stride| { self.cx.intern(AttrSetDef { - attrs: [Attr::SpvAnnotation(spv::Inst { - opcode: wk.OpDecorate, - imms: [ - spv::Imm::Short(wk.Decoration, wk.ArrayStride), - spv::Imm::Short(wk.LiteralInteger, stride.get()), - ] - .into_iter() - .collect(), - })] + attrs: [Attr::SpvAnnotation { + spv_inst: spv::Inst { + opcode: wk.OpDecorate, + imms: [ + spv::Imm::Short(wk.Decoration, wk.ArrayStride), + spv::Imm::Short(wk.LiteralInteger, stride.get()), + ] + .into_iter() + .collect(), + }, + per_instance_const_inputs: None, + }] .into(), }) }); @@ -310,16 +316,19 @@ impl<'a> LiftToSpvPtrs<'a> { SmallVec::with_capacity(field_offsets_and_types.size_hint().0); for (i, field_offset_and_type) in field_offsets_and_types.enumerate() { let (offset, field_type) = field_offset_and_type?; - attrs.attrs.insert(Attr::SpvAnnotation(spv::Inst { - opcode: wk.OpMemberDecorate, - imms: [ - spv::Imm::Short(wk.LiteralInteger, i.try_into().unwrap()), - spv::Imm::Short(wk.Decoration, wk.Offset), - spv::Imm::Short(wk.LiteralInteger, offset), - ] - .into_iter() - .collect(), - })); + attrs.attrs.insert(Attr::SpvAnnotation { + spv_inst: spv::Inst { + opcode: wk.OpMemberDecorate, + imms: [ + spv::Imm::Short(wk.LiteralInteger, i.try_into().unwrap()), + spv::Imm::Short(wk.Decoration, wk.Offset), + spv::Imm::Short(wk.LiteralInteger, offset), + ] + .into_iter() + .collect(), + }, + per_instance_const_inputs: None, + }); type_and_const_inputs.push(TypeOrConst::Type(field_type)); } attrs.attrs.extend(extra_attrs); diff --git a/src/spv/lift.rs b/src/spv/lift.rs index 5bbb6551..9ad93e0f 100644 --- a/src/spv/lift.rs +++ b/src/spv/lift.rs @@ -1506,25 +1506,39 @@ impl Module { | Attr::Diagnostics(_) | Attr::QPtr(_) | Attr::SpvBitflagsOperand(_) => {} - Attr::SpvAnnotation(inst @ spv::Inst { opcode, .. }) => { + Attr::SpvAnnotation { + spv_inst: inst @ spv::Inst { opcode, .. }, + per_instance_const_inputs, + } => { let target_id = result_id.expect( "FIXME: it shouldn't be possible to attach \ - attributes to instructions without an output", + attributes to instructions without an output", ); - let inst = spv::InstWithIds { + let empty_const_inputs = SmallVec::new(); + let instances = per_instance_const_inputs + .as_ref() + .map_or(std::slice::from_ref(&empty_const_inputs), |instances| { + &instances.0[..] + }); + + let insts = instances.iter().map(|const_inputs| spv::InstWithIds { without_ids: inst.clone(), result_type_id: None, result_id: None, - ids: iter::once(target_id).collect(), - }; + ids: iter::once(target_id) + .chain( + const_inputs.iter().map(|&ct| ids.globals[&Global::Const(ct)]), + ) + .collect(), + }); if [wk.OpExecutionMode, wk.OpExecutionModeId].contains(opcode) { - execution_mode_insts.push(inst); + execution_mode_insts.extend(insts); } else if [wk.OpName, wk.OpMemberName].contains(opcode) { - debug_name_insts.push(inst); + debug_name_insts.extend(insts); } else { - decoration_insts.push(inst); + decoration_insts.extend(insts); } } } diff --git a/src/spv/lower.rs b/src/spv/lower.rs index e11023b1..0af303b8 100644 --- a/src/spv/lower.rs +++ b/src/spv/lower.rs @@ -182,7 +182,8 @@ impl Module { let mut seq = None; let mut has_memory_model = false; - let mut pending_attrs = FxHashMap::::default(); + let mut pending_spv_annotations = + FxHashMap::)>>::default(); let mut pending_imports = FxHashMap::::default(); let mut pending_exports = vec![]; let mut current_dbg_src_loc = None; @@ -253,8 +254,57 @@ impl Module { } current_block_id = new_block_id; - let mut attrs = - inst.result_id.and_then(|id| pending_attrs.remove(&id)).unwrap_or_default(); + let mut attrs = crate::AttrSetDef::default(); + + { + let annotations = inst + .result_id + .and_then(|id| pending_spv_annotations.remove(&id)) + .unwrap_or_default(); + + // HACK(eddyb) when `per_instance_const_inputs` is `Some` in + // `Attr::SpvAnnotation`, it must collect all "instances", + // i.e. all occurrences of the same `spv_inst` value, so this + // map papers over `Attr` not having a key/value split itself. + let mut instances_per_spv_inst: FxIndexMap> = + FxIndexMap::default(); + + for (spv_inst, operand_ids) in annotations { + if operand_ids.is_empty() { + attrs.attrs.insert(Attr::SpvAnnotation { + spv_inst, + per_instance_const_inputs: None, + }); + continue; + } + + let const_inputs = operand_ids + .iter() + .map(|&id| match id_defs.get(&id) { + Some(&IdDef::Const(ct)) => Ok(ct), + Some(id_def) => Err(id_def.descr(&cx)), + None => Err(format!("a forward reference to %{id}")), + }) + .map(|result| { + result.map_err(|descr| { + invalid(&format!( + "unsupported use of {descr} in `{}`", + spv_inst.opcode.name() + )) + }) + }) + .collect::>()?; + + instances_per_spv_inst.entry(spv_inst).or_default().push(const_inputs); + } + + attrs.attrs.extend(instances_per_spv_inst.into_iter().map( + |(spv_inst, instances)| Attr::SpvAnnotation { + spv_inst, + per_instance_const_inputs: Some(crate::OrdAssertEq(Rc::new(instances))), + }, + )); + } if let Some(dbg_src_loc) = current_dbg_src_loc { attrs.set_dbg_src_loc(dbg_src_loc); @@ -477,23 +527,23 @@ impl Module { Seq::EntryPoint } else if [ wk.OpExecutionMode, - wk.OpExecutionModeId, // FIXME(eddyb) not actually supported + wk.OpExecutionModeId, wk.OpName, wk.OpMemberName, wk.OpDecorate, - wk.OpMemberDecorate, - wk.OpDecorateId, // FIXME(eddyb) not actually supported + wk.OpDecorateId, wk.OpDecorateString, + wk.OpMemberDecorate, + wk.OpMemberDecorateIdEXT, wk.OpMemberDecorateString, ] .contains(&opcode) { assert!(inst.result_type_id.is_none() && inst.result_id.is_none()); - let target_id = inst.ids[0]; - if inst.ids.len() > 1 { - return Err(invalid("unsupported decoration with ID")); - } + let target_id = inst.ids.remove(0); + let operand_ids = inst.ids; + let inst = inst.without_ids; match inst.imms[..] { // Special-case `OpDecorate LinkageAttributes ... Import|Export`. @@ -506,6 +556,8 @@ impl Module { && lt_kind == wk.LinkageType && [wk.Import, wk.Export].contains(&linkage_type) => { + assert_eq!(operand_ids.len(), 0); + let name = spv::extract_literal_string(name) .map_err(|e| invalid(&format!("{} in {:?}", e, e.as_bytes())))?; let name = cx.intern(name); @@ -518,11 +570,10 @@ impl Module { } _ => { - pending_attrs + pending_spv_annotations .entry(target_id) .or_default() - .attrs - .insert(Attr::SpvAnnotation(inst.without_ids)); + .push((inst, operand_ids)); } }; @@ -817,9 +868,9 @@ impl Module { return Err(invalid("missing OpMemoryModel")); } - if !pending_attrs.is_empty() { - let ids = pending_attrs.keys().collect::>(); - return Err(invalid(&format!("decorated IDs never defined: {ids:?}"))); + if !pending_spv_annotations.is_empty() { + let ids = pending_spv_annotations.keys().collect::>(); + return Err(invalid(&format!("annotated IDs never defined: {ids:?}"))); } if current_func_body.is_some() { diff --git a/src/spv/spec.rs b/src/spv/spec.rs index 917089e2..744e2475 100644 --- a/src/spv/spec.rs +++ b/src/spv/spec.rs @@ -103,9 +103,10 @@ def_well_known! { OpModuleProcessed, OpDecorate, - OpMemberDecorate, OpDecorateId, OpDecorateString, + OpMemberDecorate, + OpMemberDecorateIdEXT, OpMemberDecorateString, // Deprecated in favor of `OpDecorate`/`OpMemberDecorate`. diff --git a/src/transform.rs b/src/transform.rs index 74670d34..30a38ab7 100644 --- a/src/transform.rs +++ b/src/transform.rs @@ -316,9 +316,7 @@ impl InnerTransform for AttrSetDef { impl InnerTransform for Attr { fn inner_transform_with(&self, transformer: &mut impl Transformer) -> Transformed { match self { - Attr::Diagnostics(_) | Attr::SpvAnnotation(_) | Attr::SpvBitflagsOperand(_) => { - Transformed::Unchanged - } + Attr::Diagnostics(_) | Attr::SpvBitflagsOperand(_) => Transformed::Unchanged, &Attr::DbgSrcLoc(OrdAssertEq(DbgSrcLoc { file_path, @@ -374,6 +372,28 @@ impl InnerTransform for Attr { } => QPtrAttr::Usage(OrdAssertEq(usage))), } } => Attr::QPtr(attr)), + + Attr::SpvAnnotation { spv_inst, per_instance_const_inputs } => { + // FIXME(eddyb) this should be replaced with an impl of `InnerTransform` + // for `Option` or some other helper, to avoid "manual transpose". + transform!({ + per_instance_const_inputs -> per_instance_const_inputs + .as_ref() + .map(|OrdAssertEq(instances)| { + Transformed::map_iter(instances.iter(), |const_inputs| { + Transformed::map_iter(const_inputs.iter(), |&ct| { + transformer.transform_const_use(ct) + }) + .map(|new_iter| new_iter.collect()) + }) + .map(|new_iter| OrdAssertEq(Rc::new(new_iter.collect()))) + }) + .map_or(Transformed::Unchanged, |t| t.map(Some)), + } => Attr::SpvAnnotation { + spv_inst: spv_inst.clone(), + per_instance_const_inputs, + }) + } } } } diff --git a/src/visit.rs b/src/visit.rs index 1829c466..c8fff755 100644 --- a/src/visit.rs +++ b/src/visit.rs @@ -232,7 +232,7 @@ impl InnerVisit for AttrSetDef { impl InnerVisit for Attr { fn inner_visit_with<'a>(&'a self, visitor: &mut impl Visitor<'a>) { match self { - Attr::Diagnostics(_) | Attr::SpvAnnotation(_) | Attr::SpvBitflagsOperand(_) => {} + Attr::Diagnostics(_) | Attr::SpvBitflagsOperand(_) => {} &Attr::DbgSrcLoc(OrdAssertEq(DbgSrcLoc { file_path: _, @@ -253,6 +253,16 @@ impl InnerVisit for Attr { QPtrAttr::Usage(usage) => usage.0.inner_visit_with(visitor), }, + + Attr::SpvAnnotation { spv_inst: _, per_instance_const_inputs } => { + if let Some(per_instance_const_inputs) = per_instance_const_inputs { + for const_inputs in &per_instance_const_inputs.0[..] { + for &ct in const_inputs { + visitor.visit_const_use(ct); + } + } + } + } } } } From 44eb9c45288649f55cc902222d897269a6a59d4e Mon Sep 17 00:00:00 2001 From: Eduard-Mihai Burtescu Date: Fri, 9 Oct 2026 16:38:35 +0300 Subject: [PATCH 3/4] Embed `Type`s in values via `ConstKind::SpvTypeOperand`, for SPIR-V instructions taking types. --- src/lib.rs | 7 ++++ src/print/mod.rs | 15 ++++++--- src/qptr/layout.rs | 3 ++ src/spv/lift.rs | 80 +++++++++++++++++++++++++++++++++------------- src/spv/lower.rs | 19 ++++++++--- src/transform.rs | 9 +++++- src/visit.rs | 5 ++- 7 files changed, 105 insertions(+), 33 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 872000f5..9a7e3d32 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -565,6 +565,10 @@ pub enum TypeKind { type_and_const_inputs: SmallVec<[TypeOrConst; 2]>, }, + /// The type of a [`ConstKind::SpvTypeOperand`] constant, i.e. a [`Type`] + /// embedded in a [`Value`] to be passed as a SPIR-V operand. + SpvTypeOperand, + /// The type of a [`ConstKind::SpvStringLiteralForExtInst`] constant, i.e. /// a SPIR-V `OpString` with no actual type in SPIR-V. SpvStringLiteralForExtInst, @@ -608,6 +612,9 @@ pub enum ConstKind { spv_inst_and_const_inputs: Rc<(spv::Inst, SmallVec<[Const; 4]>)>, }, + /// Type used as an operand to a SPIR-V instruction (other than result type). + SpvTypeOperand(Type), + /// SPIR-V `OpString`, but only when used as an operand for an `OpExtInst`, /// which can't have literals itself - for non-string literals `OpConstant*` /// are readily usable, but only `OpString` is supported for string literals. diff --git a/src/print/mod.rs b/src/print/mod.rs index c852da38..55d87064 100644 --- a/src/print/mod.rs +++ b/src/print/mod.rs @@ -1137,9 +1137,9 @@ impl<'a> Printer<'a> { || type_and_const_inputs.is_empty() } - TypeKind::QPtr | TypeKind::SpvStringLiteralForExtInst => { - true - } + TypeKind::QPtr + | TypeKind::SpvTypeOperand + | TypeKind::SpvStringLiteralForExtInst => true, }; ty_def.attrs == AttrSet::default() @@ -3218,6 +3218,12 @@ impl Print for TypeDef { TypeOrConst::Const(ct) => ct.print(printer), }), ), + TypeKind::SpvTypeOperand => pretty::Fragment::new([ + printer.error_style().apply("type_of"), + "(".into(), + printer.declarative_keyword_style().apply("type"), + ")".into(), + ]), TypeKind::SpvStringLiteralForExtInst => pretty::Fragment::new([ printer.error_style().apply("type_of").into(), "(".into(), @@ -3388,6 +3394,7 @@ impl Print for ConstDef { printer.pretty_type_ascription_suffix(*ty), ]) } + ConstKind::SpvTypeOperand(ty) => ty.print(printer), &ConstKind::SpvStringLiteralForExtInst(s) => pretty::Fragment::new([ printer.pretty_spv_opcode(printer.spv_op_style(), wk.OpString), "(".into(), @@ -4139,7 +4146,7 @@ impl Print for FuncAt<'_, DataInst> { } } } - ConstKind::PtrToGlobalVar(_) => {} + ConstKind::PtrToGlobalVar(_) | ConstKind::SpvTypeOperand(_) => {} } } None diff --git a/src/qptr/layout.rs b/src/qptr/layout.rs index 8837eb60..d09fe1d0 100644 --- a/src/qptr/layout.rs +++ b/src/qptr/layout.rs @@ -216,6 +216,9 @@ impl<'a> LayoutCache<'a> { TypeKind::SpvInst { spv_inst, type_and_const_inputs } => { (spv_inst, type_and_const_inputs) } + TypeKind::SpvTypeOperand => { + return Err(LayoutError(Diag::bug(["`layout_of(type_of(type))`".into()]))); + } TypeKind::SpvStringLiteralForExtInst => { return Err(LayoutError(Diag::bug([ "`layout_of(type_of(OpString<\"...\">))`".into() diff --git a/src/spv/lift.rs b/src/spv/lift.rs index 9ad93e0f..9d48425e 100644 --- a/src/spv/lift.rs +++ b/src/spv/lift.rs @@ -127,6 +127,14 @@ impl Visitor<'_> for NeedsIdsCollector<'_> { } TypeKind::SpvInst { .. } => {} + + TypeKind::SpvTypeOperand => { + unreachable!( + "`TypeKind::SpvTypeOperand` should not be used \ + as a type outside of `ConstKind::SpvTypeOperand`" + ); + } + TypeKind::SpvStringLiteralForExtInst => { unreachable!( "`TypeKind::SpvStringLiteralForExtInst` should not be used \ @@ -149,6 +157,21 @@ impl Visitor<'_> for NeedsIdsCollector<'_> { self.globals.insert(global); } + // HACK(eddyb) because this is merely a way to refer to `wrapped_type` + // as a `Value` (for a SPIR-V operand), it needs to be special-cased, + // without visiting its type, or an entry in `self.globals`. + ConstKind::SpvTypeOperand(wrapped_type) => { + let ConstDef { attrs, ty, kind: _ } = ct_def; + + assert!(*attrs == AttrSet::default()); + assert!( + self.cx[*ty] + == TypeDef { attrs: AttrSet::default(), kind: TypeKind::SpvTypeOperand } + ); + + self.visit_type_use(wrapped_type); + } + // HACK(eddyb) because this is an `OpString` and needs to go earlier // in the module than any `OpConstant*`, it needs to be special-cased, // without visiting its type, or an entry in `self.globals`. @@ -229,6 +252,9 @@ impl Visitor<'_> for NeedsIdsCollector<'_> { } struct AllocatedIds<'a> { + // HACK(eddyb) only for `Index` impl. + cx: &'a Context, + ext_inst_imports: BTreeMap<&'a str, spv::Id>, debug_strings: BTreeMap<&'a str, spv::Id>, @@ -238,6 +264,19 @@ struct AllocatedIds<'a> { funcs: FxIndexMap>, } +// HACK(eddyb) this makes accessing the right ID a bit easier. +impl std::ops::Index for AllocatedIds<'_> { + type Output = spv::Id; + fn index(&self, ct: Const) -> &Self::Output { + match self.cx[ct].kind { + ConstKind::SpvTypeOperand(ty) => &self.globals[&Global::Type(ty)], + ConstKind::SpvStringLiteralForExtInst(s) => &self.debug_strings[&self.cx[s]], + + _ => &self.globals[&Global::Const(ct)], + } + } +} + // FIXME(eddyb) should this use ID ranges instead of `SmallVec<[spv::Id; 4]>`? struct FuncLifting<'a> { func_id: spv::Id, @@ -358,6 +397,8 @@ impl<'a> NeedsIdsCollector<'a> { } = self; Ok(AllocatedIds { + cx, + ext_inst_imports: ext_inst_imports .into_iter() .map(|name| Ok((name, alloc_id()?))) @@ -1035,7 +1076,8 @@ impl LazyInst<'_, '_> { ConstKind::SpvInst { .. } => (ct_def.attrs, None), // Not inserted into `globals` while visiting. - ConstKind::SpvStringLiteralForExtInst(_) => unreachable!(), + ConstKind::SpvTypeOperand(_) + | ConstKind::SpvStringLiteralForExtInst(_) => unreachable!(), } } }; @@ -1069,11 +1111,7 @@ impl LazyInst<'_, '_> { let cx = module.cx_ref(); let value_to_id = |parent_func: &FuncLifting<'_>, v| match v { - Value::Const(ct) => match cx[ct].kind { - ConstKind::SpvStringLiteralForExtInst(s) => ids.debug_strings[&cx[s]], - - _ => ids.globals[&Global::Const(ct)], - }, + Value::Const(ct) => ids[ct], Value::RegionInput { region, input_idx } => { let input_idx = usize::try_from(input_idx).unwrap(); match parent_func.region_inputs_source.get(®ion) { @@ -1105,17 +1143,17 @@ impl LazyInst<'_, '_> { result_id, ids: type_and_const_inputs .iter() - .map(|&ty_or_ct| { - ids.globals[&match ty_or_ct { - TypeOrConst::Type(ty) => Global::Type(ty), - TypeOrConst::Const(ct) => Global::Const(ct), - }] + .map(|&ty_or_ct| match ty_or_ct { + TypeOrConst::Type(ty) => ids.globals[&Global::Type(ty)], + TypeOrConst::Const(ct) => ids[ct], }) .collect(), }, // Not inserted into `globals` while visiting. - TypeKind::QPtr | TypeKind::SpvStringLiteralForExtInst => unreachable!(), + TypeKind::QPtr + | TypeKind::SpvTypeOperand + | TypeKind::SpvStringLiteralForExtInst => unreachable!(), }, Global::Const(ct) => { let ct_def = &cx[ct]; @@ -1139,8 +1177,9 @@ impl LazyInst<'_, '_> { }; let initializer = match gv_decl.def { DeclDef::Imported(_) => None, - DeclDef::Present(GlobalVarDefBody { initializer }) => initializer - .map(|initializer| ids.globals[&Global::Const(initializer)]), + DeclDef::Present(GlobalVarDefBody { initializer }) => { + initializer.map(|initializer| ids[initializer]) + } }; spv::InstWithIds { without_ids: spv::Inst { @@ -1159,15 +1198,14 @@ impl LazyInst<'_, '_> { without_ids: spv_inst.clone(), result_type_id: Some(ids.globals[&Global::Type(ct_def.ty)]), result_id, - ids: const_inputs - .iter() - .map(|&ct| ids.globals[&Global::Const(ct)]) - .collect(), + ids: const_inputs.iter().map(|&ct| ids[ct]).collect(), } } // Not inserted into `globals` while visiting. - ConstKind::SpvStringLiteralForExtInst(_) => unreachable!(), + ConstKind::SpvTypeOperand(_) | ConstKind::SpvStringLiteralForExtInst(_) => { + unreachable!() + } } } }, @@ -1527,9 +1565,7 @@ impl Module { result_type_id: None, result_id: None, ids: iter::once(target_id) - .chain( - const_inputs.iter().map(|&ct| ids.globals[&Global::Const(ct)]), - ) + .chain(const_inputs.iter().map(|&ct| ids[ct])) .collect(), }); diff --git a/src/spv/lower.rs b/src/spv/lower.rs index 0af303b8..601d56fe 100644 --- a/src/spv/lower.rs +++ b/src/spv/lower.rs @@ -110,6 +110,15 @@ impl Module { let spv_spec = spec::Spec::get(); let wk = &spv_spec.well_known; + // HACK(eddyb) useful for embedding `Type` in `Const` whenever necessary. + let spv_type_operand_as_const = |ty| { + cx.intern(ConstDef { + attrs: AttrSet::default(), + ty: cx.intern(TypeKind::SpvTypeOperand), + kind: ConstKind::SpvTypeOperand(ty), + }) + }; + // HACK(eddyb) used to quickly check whether an `OpVariable` is global. let storage_class_function_imm = spv::Imm::Short(wk.StorageClass, wk.Function); @@ -281,6 +290,7 @@ impl Module { let const_inputs = operand_ids .iter() .map(|&id| match id_defs.get(&id) { + Some(&IdDef::Type(ty)) => Ok(spv_type_operand_as_const(ty)), Some(&IdDef::Const(ct)) => Ok(ct), Some(id_def) => Err(id_def.descr(&cx)), None => Err(format!("a forward reference to %{id}")), @@ -640,6 +650,7 @@ impl Module { .ids .iter() .map(|&id| match id_defs.get(&id) { + Some(&IdDef::Type(ty)) => Ok(spv_type_operand_as_const(ty)), Some(&IdDef::Const(ct)) => Ok(ct), Some(id_def) => Err(id_def.descr(&cx)), None => Err(format!("a forward reference to %{id}")), @@ -1188,12 +1199,10 @@ impl Module { // FIXME(eddyb) this returns `LocalIdDef` even for global values. let lookup_global_or_local_id_for_data_or_control_inst_input = |id| match id_defs.get(&id) { + Some(&IdDef::Type(ty)) => { + Ok(LocalIdDef::Value(Value::Const(spv_type_operand_as_const(ty)))) + } Some(&IdDef::Const(ct)) => Ok(LocalIdDef::Value(Value::Const(ct))), - Some(id_def @ IdDef::Type(_)) => Err(invalid(&format!( - "unsupported use of {} as an operand for \ - an instruction in a function", - id_def.descr(&cx), - ))), Some(id_def @ IdDef::Func(_)) => Err(invalid(&format!( "unsupported use of {} outside `OpFunctionCall`", id_def.descr(&cx), diff --git a/src/transform.rs b/src/transform.rs index 30a38ab7..c691b5ac 100644 --- a/src/transform.rs +++ b/src/transform.rs @@ -451,7 +451,9 @@ impl InnerTransform for TypeDef { transform!({ attrs -> transformer.transform_attr_set_use(*attrs), kind -> match kind { - TypeKind::QPtr | TypeKind::SpvStringLiteralForExtInst => Transformed::Unchanged, + TypeKind::QPtr + | TypeKind::SpvTypeOperand + | TypeKind::SpvStringLiteralForExtInst => Transformed::Unchanged, TypeKind::SpvInst { spv_inst, type_and_const_inputs } => Transformed::map_iter( type_and_const_inputs.iter(), @@ -497,6 +499,11 @@ impl InnerTransform for ConstDef { spv_inst_and_const_inputs: Rc::new((spv_inst.clone(), new_iter.collect())), }) } + + ConstKind::SpvTypeOperand(ty) => transform!({ + ty -> transformer.transform_type_use(*ty), + } => ConstKind::SpvTypeOperand(ty)), + ConstKind::SpvStringLiteralForExtInst(_) => Transformed::Unchanged }, } => Self { diff --git a/src/visit.rs b/src/visit.rs index c8fff755..d5577687 100644 --- a/src/visit.rs +++ b/src/visit.rs @@ -329,7 +329,7 @@ impl InnerVisit for TypeDef { visitor.visit_attr_set_use(*attrs); match kind { - TypeKind::QPtr | TypeKind::SpvStringLiteralForExtInst => {} + TypeKind::QPtr | TypeKind::SpvTypeOperand | TypeKind::SpvStringLiteralForExtInst => {} TypeKind::SpvInst { spv_inst: _, type_and_const_inputs } => { for &ty_or_ct in type_and_const_inputs { @@ -357,6 +357,9 @@ impl InnerVisit for ConstDef { visitor.visit_const_use(ct); } } + + ConstKind::SpvTypeOperand(ty) => visitor.visit_type_use(*ty), + ConstKind::SpvStringLiteralForExtInst(_) => {} } } From 64d7eb37ad1fda1d81ef0cb39fb84ec73a286902 Mon Sep 17 00:00:00 2001 From: Eduard-Mihai Burtescu Date: Thu, 18 Dec 2025 10:22:38 +0200 Subject: [PATCH 4/4] spv/lower: support illegal forward refs to global `OpVariable`s (for initializer cycles). --- src/spv/lower.rs | 162 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 139 insertions(+), 23 deletions(-) diff --git a/src/spv/lower.rs b/src/spv/lower.rs index 601d56fe..6f039ceb 100644 --- a/src/spv/lower.rs +++ b/src/spv/lower.rs @@ -9,6 +9,7 @@ use crate::{ Module, NodeDef, NodeKind, Region, RegionDef, RegionInputDecl, SelectionKind, Type, TypeDef, TypeKind, TypeOrConst, Value, cfg, print, }; +use itertools::Itertools as _; use rustc_hash::FxHashMap; use smallvec::SmallVec; use std::collections::{BTreeMap, BTreeSet}; @@ -22,6 +23,9 @@ enum IdDef { Type(Type), Const(Const), + // HACK(eddyb) for illegal cycles caused by global initializers. + PtrToGlobalVarForwardRef(Const), + Func(Func), SpvExtInstImport(InternedStr), @@ -34,7 +38,7 @@ impl IdDef { // FIXME(eddyb) print these with some kind of "maximum depth", // instead of just describing the kind of definition. IdDef::Type(_) => "a type".into(), - IdDef::Const(_) => "a constant".into(), + IdDef::Const(_) | IdDef::PtrToGlobalVarForwardRef(_) => "a constant".into(), IdDef::Func(_) => "a function".into(), @@ -122,6 +126,52 @@ impl Module { // HACK(eddyb) used to quickly check whether an `OpVariable` is global. let storage_class_function_imm = spv::Imm::Short(wk.StorageClass, wk.Function); + // HACK(eddyb) used as the `GlobalVarDecl` for an `IdDef::PtrToGlobalVarForwardRef`. + let dummy_decl_for_global_var_forward_ref = + |spv_inst: &spv::InstWithIds, type_of_ptr_to| { + assert!(spv_inst.opcode == wk.OpVariable); + + let storage_class = match spv_inst.imms[..] { + [spv::Imm::Short(kind, storage_class)] => { + assert_eq!(kind, wk.StorageClass); + storage_class + } + _ => unreachable!(), + }; + + GlobalVarDecl { + attrs: { + let mut attrs = AttrSet::default(); + attrs.push_diag( + &cx, + Diag::err([ + "global ID used as forward reference but never defined".into() + ]), + ); + attrs + }, + type_of_ptr_to, + shape: None, + addr_space: AddrSpace::SpvStorageClass(storage_class), + def: DeclDef::Imported(Import::LinkName(cx.intern(""))), + } + }; + // HACK(eddyb) no `PartialEq` on `GlobalVarDecl`. + let assert_is_dummy_decl_for_global_var_forward_ref = + |decl: &GlobalVarDecl, spv_inst: &spv::InstWithIds| { + let dummy_decl = + dummy_decl_for_global_var_forward_ref(spv_inst, decl.type_of_ptr_to); + let [expected, found] = [&dummy_decl, decl].map( + |GlobalVarDecl { attrs, type_of_ptr_to, shape, addr_space, def }| { + let DeclDef::Imported(import) = def else { + unreachable!(); + }; + (attrs, type_of_ptr_to, shape, addr_space, import) + }, + ); + assert!(expected == found); + }; + let mut module = { let [magic, version, generator_magic, id_bound, reserved_inst_schema] = parser.header; @@ -201,7 +251,7 @@ impl Module { let mut pending_func_bodies = vec![]; let mut current_func_body = None; - let mut spv_insts = parser.peekable(); + let mut spv_insts = parser.multipeek(); while let Some(mut inst) = spv_insts.next().transpose()? { let opcode = inst.opcode; @@ -464,6 +514,7 @@ impl Module { // Absorb all following `OpSourceContinued` into `contents`. while let Some(Ok(cont_inst)) = spv_insts.peek() { if cont_inst.opcode != wk.OpSourceContinued { + spv_insts.reset_peek(); break; } let cont_inst = spv_insts.next().unwrap().unwrap(); @@ -627,8 +678,10 @@ impl Module { .map(|&id| match id_defs.get(&id) { Some(&IdDef::Type(ty)) => Ok(TypeOrConst::Type(ty)), Some(&IdDef::Const(ct)) => Ok(TypeOrConst::Const(ct)), + None | Some(IdDef::PtrToGlobalVarForwardRef(_)) => { + Err(format!("a forward reference to %{id}")) + } Some(id_def) => Err(id_def.descr(&cx)), - None => Err(format!("a forward reference to %{id}")), }) .map(|result| { result.map_err(|descr| { @@ -651,9 +704,44 @@ impl Module { .iter() .map(|&id| match id_defs.get(&id) { Some(&IdDef::Type(ty)) => Ok(spv_type_operand_as_const(ty)), - Some(&IdDef::Const(ct)) => Ok(ct), + Some(&IdDef::Const(ct) | &IdDef::PtrToGlobalVarForwardRef(ct)) => Ok(ct), Some(id_def) => Err(id_def.descr(&cx)), - None => Err(format!("a forward reference to %{id}")), + None => { + // HACK(eddyb) search for a matching `OpVariable` + // later on, just in case this is a cycle caused + // by the initializer value of a global. + let mut found = None; + while let Some(Ok(later_inst)) = spv_insts.peek() { + if later_inst.opcode == wk.OpFunction { + break; + } + if later_inst.opcode == wk.OpVariable + && later_inst.result_id == Some(id) + && let Some(&IdDef::Type(ty)) = + id_defs.get(&later_inst.result_type_id.unwrap()) + { + let global_var = module.global_vars.define( + &cx, + dummy_decl_for_global_var_forward_ref(later_inst, ty), + ); + let ptr_to_global_var = cx.intern(ConstDef { + attrs: AttrSet::default(), + ty, + kind: ConstKind::PtrToGlobalVar(global_var), + }); + id_defs.insert( + id, + IdDef::PtrToGlobalVarForwardRef(ptr_to_global_var), + ); + found = Some(ptr_to_global_var); + break; + } + } + + let ct = + found.ok_or_else(|| format!("a forward reference to %{id}"))?; + Ok(ct) + } }) .map(|result| { result.map_err(|descr| { @@ -702,8 +790,10 @@ impl Module { let initializer = initializer .map(|id| match id_defs.get(&id) { Some(&IdDef::Const(ct)) => Ok(ct), + None | Some(IdDef::PtrToGlobalVarForwardRef(_)) => { + Err(format!("a forward reference to %{id}")) + } Some(id_def) => Err(id_def.descr(&cx)), - None => Err(format!("a forward reference to %{id}")), }) .transpose() .map_err(|descr| { @@ -725,22 +815,48 @@ impl Module { None => DeclDef::Present(GlobalVarDefBody { initializer }), }; - let global_var = module.global_vars.define( - &cx, - GlobalVarDecl { - attrs: mem::take(&mut attrs), - type_of_ptr_to: type_of_ptr_to_global_var, - shape: None, - addr_space: AddrSpace::SpvStorageClass(storage_class), - def, - }, - ); - let ptr_to_global_var = cx.intern(ConstDef { - attrs: AttrSet::default(), - ty: type_of_ptr_to_global_var, - kind: ConstKind::PtrToGlobalVar(global_var), - }); - id_defs.insert(global_var_id, IdDef::Const(ptr_to_global_var)); + let decl = GlobalVarDecl { + attrs: mem::take(&mut attrs), + type_of_ptr_to: type_of_ptr_to_global_var, + shape: None, + addr_space: AddrSpace::SpvStorageClass(storage_class), + def, + }; + + { + use std::collections::hash_map::Entry; + + match id_defs.entry(global_var_id) { + Entry::Occupied(mut entry) => match entry.get() { + &IdDef::PtrToGlobalVarForwardRef(ct) => { + let ConstKind::PtrToGlobalVar(global_var) = cx[ct].kind else { + unreachable!(); + }; + let decl_slot = &mut module.global_vars[global_var]; + assert_is_dummy_decl_for_global_var_forward_ref(decl_slot, &inst); + *decl_slot = decl; + + entry.insert(IdDef::Const(ct)); + } + id_def => { + return Err(invalid(&format!( + "invalid redefinition of {} as a new global", + id_def.descr(&cx) + ))); + } + }, + Entry::Vacant(entry) => { + let ptr_to_global_var = cx.intern(ConstDef { + attrs: AttrSet::default(), + ty: type_of_ptr_to_global_var, + kind: ConstKind::PtrToGlobalVar( + module.global_vars.define(&cx, decl), + ), + }); + entry.insert(IdDef::Const(ptr_to_global_var)); + } + } + } Seq::TypeConstOrGlobalVar } else if opcode == wk.OpFunction { @@ -1230,7 +1346,7 @@ impl Module { "unsupported use of {} outside `OpExtInst`", id_def.descr(&cx), ))), - None => local_id_defs + None | Some(IdDef::PtrToGlobalVarForwardRef(_)) => local_id_defs .get(&id) .copied() .ok_or_else(|| invalid(&format!("undefined ID %{id}",))),