diff --git a/rspirv-linker/Cargo.lock b/rspirv-linker/Cargo.lock index a5db2f0380..60a7dbd973 100644 --- a/rspirv-linker/Cargo.lock +++ b/rspirv-linker/Cargo.lock @@ -9,6 +9,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + [[package]] name = "autocfg" version = "1.0.1" @@ -27,6 +36,22 @@ version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "ctor" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39858aa5bac06462d4dd4b9164848eb81ffc4aa5c479746393598fd193afa227" +dependencies = [ + "quote 1.0.7", + "syn 1.0.39", +] + [[package]] name = "derive_more" version = "0.15.0" @@ -34,13 +59,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a141330240c921ec6d074a3e188a7c7ef95668bb95e7d44fa0e5778ec2a7afe" dependencies = [ "lazy_static", - "proc-macro2", - "quote", + "proc-macro2 0.4.30", + "quote 0.6.13", "regex", "rustc_version", - "syn", + "syn 0.15.44", ] +[[package]] +name = "difference" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" + [[package]] name = "fxhash" version = "0.2.1" @@ -50,12 +81,29 @@ dependencies = [ "byteorder", ] +[[package]] +name = "getrandom" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "libc" +version = "0.2.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "755456fae044e6fa1ebbbd1b3e902ae19e73097ed4ed87bb79934a867c007bc3" + [[package]] name = "memchr" version = "2.3.3" @@ -71,13 +119,49 @@ dependencies = [ "autocfg", ] +[[package]] +name = "output_vt100" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9" +dependencies = [ + "winapi", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c36fa947111f5c62a733b652544dd0016a43ce89619538a8ef92724a6f501a20" + +[[package]] +name = "pretty_assertions" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f81e1644e1b54f5a68959a29aa86cde704219254669da328ecfdf6a1f09d427" +dependencies = [ + "ansi_term", + "ctor", + "difference", + "output_vt100", +] + [[package]] name = "proc-macro2" version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" dependencies = [ - "unicode-xid", + "unicode-xid 0.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f5f085b5d71e2188cb8271e5da0161ad52c3f227a661a3c135fdf28e258b12" +dependencies = [ + "unicode-xid 0.2.1", ] [[package]] @@ -86,9 +170,65 @@ version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" dependencies = [ - "proc-macro2", + "proc-macro2 0.4.30", ] +[[package]] +name = "quote" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +dependencies = [ + "proc-macro2 1.0.19", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + [[package]] name = "regex" version = "1.3.9" @@ -107,6 +247,15 @@ version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + [[package]] name = "rspirv" version = "0.7.0" @@ -121,7 +270,10 @@ dependencies = [ name = "rspirv-linker" version = "0.1.0" dependencies = [ + "pretty_assertions", "rspirv", + "tempfile", + "topological-sort", ] [[package]] @@ -162,9 +314,34 @@ version = "0.15.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", + "proc-macro2 0.4.30", + "quote 0.6.13", + "unicode-xid 0.1.0", +] + +[[package]] +name = "syn" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d8d6567fe7c7f8835a3a98af4208f3846fba258c1bc3c31d6e506239f11f9" +dependencies = [ + "proc-macro2 1.0.19", + "quote 1.0.7", + "unicode-xid 0.2.1", +] + +[[package]] +name = "tempfile" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" +dependencies = [ + "cfg-if", + "libc", + "rand", + "redox_syscall", + "remove_dir_all", + "winapi", ] [[package]] @@ -176,8 +353,48 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "topological-sort" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa7c7f42dea4b1b99439786f5633aeb9c14c1b53f75e282803c2ec2ad545873c" + [[package]] name = "unicode-xid" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/rspirv-linker/Cargo.toml b/rspirv-linker/Cargo.toml index f6261cfe8f..67d233ccbb 100644 --- a/rspirv-linker/Cargo.toml +++ b/rspirv-linker/Cargo.toml @@ -7,4 +7,9 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -rspirv = { path = "C:/Users/Jasper/traverse/rspirv/rspirv/"} \ No newline at end of file +rspirv = { path = "C:/Users/Jasper/traverse/rspirv/rspirv/"} +topological-sort = "0.1" + +[dev-dependencies] +tempfile = "3.1" +pretty_assertions = "0.6" diff --git a/rspirv-linker/src/main.rs b/rspirv-linker/src/main.rs index d96c09fe00..b8fdbf4afa 100644 --- a/rspirv-linker/src/main.rs +++ b/rspirv-linker/src/main.rs @@ -1,8 +1,10 @@ -use rspirv::binary::Assemble; +mod test; + use rspirv::binary::Consumer; use rspirv::binary::Disassemble; use rspirv::spirv; use std::collections::{HashMap, HashSet}; +use topological_sort::TopologicalSort; fn load(bytes: &[u8]) -> rspirv::dr::Module { let mut loader = rspirv::dr::Loader::new(); @@ -104,6 +106,10 @@ fn kill_with(insts: &mut Vec, f: F) where F: Fn(&rspirv::dr::Instruction) -> bool, { + if insts.is_empty() { + return; + } + let mut idx = insts.len() - 1; // odd backwards loop so we can swap_remove loop { @@ -283,7 +289,7 @@ impl LinkInfo { } /// returns the list of matching import / export pairs after validation the list of potential pairs - fn ensure_matching_import_export_pairs(&self, defs: &DefAnalyzer) -> &Vec { + fn ensure_matching_import_export_pairs(&self) -> &Vec { for pair in &self.potential_pairs { for (import_param, export_param) in pair .import @@ -339,7 +345,28 @@ fn import_kill_annotations_and_debug(module: &mut rspirv::dr::Module, info: &Lin } } -fn kill_linkage_instructions(pairs: &Vec, module: &mut rspirv::dr::Module) { +struct Options { + /// `true` if we're creating a library + lib: bool, + + /// `true` if partial linking is allowed + partial: bool, +} + +impl Default for Options { + fn default() -> Self { + Self { + lib: false, + partial: false, + } + } +} + +fn kill_linkage_instructions( + pairs: &Vec, + module: &mut rspirv::dr::Module, + opts: &Options, +) { // drop imported functions for pair in pairs.iter() { module @@ -372,6 +399,15 @@ fn kill_linkage_instructions(pairs: &Vec, module: &mut rspirv: == rspirv::dr::Operand::Decoration(spirv::Decoration::LinkageAttributes) }); + if !opts.lib { + kill_with(&mut module.annotations, |inst| { + inst.class.opcode == spirv::Op::Decorate + && inst.operands[1] + == rspirv::dr::Operand::Decoration(spirv::Decoration::LinkageAttributes) + && inst.operands[3] == rspirv::dr::Operand::LinkageType(spirv::LinkageType::Export) + }); + } + // drop OpCapability Linkage kill_with(&mut module.capabilities, |inst| { inst.class.opcode == spirv::Op::Capability @@ -414,8 +450,52 @@ fn compact_ids(module: &mut rspirv::dr::Module) -> u32 { remap.len() as u32 + 1 } -fn link(inputs: &mut [&mut rspirv::dr::Module]) -> rspirv::dr::Module { - // 1. shift all the ids +fn sort_globals(module: &mut rspirv::dr::Module) { + let mut ts = TopologicalSort::::new(); + + for t in module.types_global_values.iter() { + if let Some(result_type) = t.result_type { + if let Some(result_id) = t.result_id { + ts.add_dependency(result_type, result_id); + + for op in &t.operands { + match op { + rspirv::dr::Operand::IdMemorySemantics(w) + | rspirv::dr::Operand::IdScope(w) + | rspirv::dr::Operand::IdRef(w) => { + ts.add_dependency(*w, result_id); // the op defining the IdRef should come before our op / result_id + } + _ => {} + } + } + } + } + } + + let defs = DefAnalyzer::new(&module); + + let mut new_types_global_values = vec![]; + + loop { + let mut v = ts.pop_all(); + v.sort(); + + for result_id in v { + new_types_global_values.push(defs.def(result_id).unwrap().clone()); + } + + if ts.is_empty() { + break; + } + } + + assert!(module.types_global_values.len() == new_types_global_values.len()); + + module.types_global_values = new_types_global_values; +} + +fn link(inputs: &mut [&mut rspirv::dr::Module], opts: &Options) -> rspirv::dr::Module { + // shift all the ids let mut bound = inputs[0].header.as_ref().unwrap().bound - 1; for mut module in inputs.iter_mut().skip(1) { @@ -423,11 +503,11 @@ fn link(inputs: &mut [&mut rspirv::dr::Module]) -> rspirv::dr::Module { bound += module.header.as_ref().unwrap().bound - 1; } - println!("{}\n\n", inputs[0].disassemble()); - println!("{}\n\n", inputs[1].disassemble()); + for i in inputs.iter() { + println!("{}\n\n", i.disassemble()); + } - // 2. generate the header (todo) - // 3. merge the binaries + // merge the binaries let mut loader = rspirv::dr::Loader::new(); for module in inputs.iter() { @@ -438,33 +518,45 @@ fn link(inputs: &mut [&mut rspirv::dr::Module]) -> rspirv::dr::Module { let mut output = loader.module(); - // 4. find import / export pairs + // find import / export pairs let defs = DefAnalyzer::new(&output); let info = find_import_export_pairs(&output, &defs); - // 5. ensure import / export pairs have matching types and defintions - let matching_pairs = info.ensure_matching_import_export_pairs(&defs); + // ensure import / export pairs have matching types and defintions + let matching_pairs = info.ensure_matching_import_export_pairs(); - // 6. remove duplicates (https://github.com/KhronosGroup/SPIRV-Tools/blob/e7866de4b1dc2a7e8672867caeb0bdca49f458d3/source/opt/remove_duplicates_pass.cpp) + // remove duplicates (https://github.com/KhronosGroup/SPIRV-Tools/blob/e7866de4b1dc2a7e8672867caeb0bdca49f458d3/source/opt/remove_duplicates_pass.cpp) remove_duplicates(&mut output); - // 7. remove names and decorations of import variables / functions https://github.com/KhronosGroup/SPIRV-Tools/blob/8a0ebd40f86d1f18ad42ea96c6ac53915076c3c7/source/opt/ir_context.cpp#L404 + // remove names and decorations of import variables / functions https://github.com/KhronosGroup/SPIRV-Tools/blob/8a0ebd40f86d1f18ad42ea96c6ac53915076c3c7/source/opt/ir_context.cpp#L404 import_kill_annotations_and_debug(&mut output, &info); - // 8. rematch import variables and functions to export variables / functions https://github.com/KhronosGroup/SPIRV-Tools/blob/8a0ebd40f86d1f18ad42ea96c6ac53915076c3c7/source/opt/ir_context.cpp#L255 + // rematch import variables and functions to export variables / functions https://github.com/KhronosGroup/SPIRV-Tools/blob/8a0ebd40f86d1f18ad42ea96c6ac53915076c3c7/source/opt/ir_context.cpp#L255 for pair in matching_pairs { replace_all_uses_with(&mut output, pair.import.id, pair.export.id); } - // 9. remove linkage specific instructions - kill_linkage_instructions(&matching_pairs, &mut output); + // remove linkage specific instructions + kill_linkage_instructions(&matching_pairs, &mut output, &opts); - // 10. compact the ids https://github.com/KhronosGroup/SPIRV-Tools/blob/e02f178a716b0c3c803ce31b9df4088596537872/source/opt/compact_ids_pass.cpp#L43 + sort_globals(&mut output); + + // compact the ids https://github.com/KhronosGroup/SPIRV-Tools/blob/e02f178a716b0c3c803ce31b9df4088596537872/source/opt/compact_ids_pass.cpp#L43 let bound = compact_ids(&mut output); - output.header = Some(rspirv::dr::ModuleHeader::new(bound)); - // 11. output the module + output.debugs.push(rspirv::dr::Instruction::new( + spirv::Op::ModuleProcessed, + None, + None, + vec![rspirv::dr::Operand::LiteralString( + "Linked by rspirv-linker".to_string(), + )], + )); + + println!("{}\n\n", output.disassemble()); + + // output the module output } @@ -475,6 +567,11 @@ fn main() { let mut body1 = load(&body1[..]); let mut body2 = load(&body2[..]); - let output = link(&mut [&mut body1, &mut body2]); + let opts = Options { + lib: false, + partial: false, + }; + + let output = link(&mut [&mut body1, &mut body2], &opts); println!("{}\n\n", output.disassemble()); } diff --git a/rspirv-linker/src/test.rs b/rspirv-linker/src/test.rs new file mode 100644 index 0000000000..b20a2f39bb --- /dev/null +++ b/rspirv-linker/src/test.rs @@ -0,0 +1,184 @@ +use crate::link; + +// https://github.com/colin-kiegel/rust-pretty-assertions/issues/24 +#[derive(PartialEq, Eq)] +#[doc(hidden)] +pub struct PrettyString<'a>(pub &'a str); +/// Make diff to display string as multi-line string +impl<'a> std::fmt::Debug for PrettyString<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str(self.0) + } +} + +fn assemble_spirv(spirv: &str) -> Vec { + use std::process::Command; + use tempfile::tempdir; + + let temp = tempdir().expect("Unable to create temp dir"); + let input = temp.path().join("code.txt"); + let output = temp.path().join("code.spv"); + + std::fs::write(&input, spirv).unwrap(); + + let process = Command::new("spirv-as.exe") + .arg(input.to_str().unwrap()) + .arg("-o") + .arg(output.to_str().unwrap()) + .output() + .expect("failed to execute process"); + + println!("status: {}", process.status); + println!("stdout: {}", String::from_utf8_lossy(&process.stdout)); + println!("stderr: {}", String::from_utf8_lossy(&process.stderr)); + + assert!(process.status.success()); + + std::fs::read(&output).unwrap() +} + +#[allow(unused)] +fn validate(spirv: &[u32]) { + use std::process::Command; + use tempfile::tempdir; + + let temp = tempdir().expect("Unable to create temp dir"); + let input = temp.path().join("code.spv"); + + let spirv = unsafe { std::slice::from_raw_parts(spirv.as_ptr() as *const u8, spirv.len() * 4) }; + + std::fs::write(&input, spirv).unwrap(); + + let process = Command::new("spirv-val.exe") + .arg(input.to_str().unwrap()) + .output() + .expect("failed to execute process"); + + println!("status: {}", process.status); + println!("stdout: {}", String::from_utf8_lossy(&process.stdout)); + println!("stderr: {}", String::from_utf8_lossy(&process.stderr)); + + assert!(process.status.success()); +} + +fn load(bytes: &[u8]) -> rspirv::dr::Module { + let mut loader = rspirv::dr::Loader::new(); + rspirv::binary::parse_bytes(&bytes, &mut loader).unwrap(); + let module = loader.module(); + module +} + +fn assemble_and_link(binaries: &[&[u8]], opts: &crate::Options) -> rspirv::dr::Module { + let mut modules = binaries.iter().cloned().map(load).collect::>(); + let mut modules = modules.iter_mut().map(|m| m).collect::>(); + + link(&mut modules, opts) +} + +fn without_header_eq(mut result: rspirv::dr::Module, expected: &str) { + use rspirv::binary::Disassemble; + //use rspirv::binary::Assemble; + + // validate(&result.assemble()); + + result.header = None; + let result = result.disassemble(); + + let expected = expected + .split("\n") + .map(|l| l.trim()) + .collect::>() + .join("\n"); + + let result = result + .split("\n") + .map(|l| l.trim().replace(" ", " ")) // rspirv outputs multiple spaces between operands + .collect::>() + .join("\n"); + + if result != expected { + panic!( + "assertion failed: `(left.contains(right))`\ + \n\ + \n{}\ + \n", + pretty_assertions::Comparison::new(&PrettyString(&result), &PrettyString(&expected)) + ) + } +} + +mod test { + use crate::test::assemble_and_link; + use crate::test::assemble_spirv; + use crate::test::without_header_eq; + use crate::Options; + + #[test] + fn standard() { + let a = assemble_spirv( + r#"OpCapability Linkage + OpDecorate %1 LinkageAttributes "foo" Import + %2 = OpTypeFloat 32 + %1 = OpVariable %2 Uniform + %3 = OpVariable %2 Input"#, + ); + + let b = assemble_spirv( + r#"OpCapability Linkage + OpDecorate %1 LinkageAttributes "foo" Export + %2 = OpTypeFloat 32 + %3 = OpConstant %2 42 + %1 = OpVariable %2 Uniform %3 + "#, + ); + + let result = assemble_and_link(&[&a, &b], &Options::default()); + let expect = r#"OpModuleProcessed "Linked by rspirv-linker" + %1 = OpTypeFloat 32 + %2 = OpVariable %1 Input + %3 = OpConstant %1 42.0 + %4 = OpVariable %1 Uniform %3"#; + + without_header_eq(result, expect); + } + + #[test] + fn not_a_lib_extra_exports() { + let a = assemble_spirv( + r#"OpCapability Linkage + OpDecorate %1 LinkageAttributes "foo" Export + %2 = OpTypeFloat 32 + %1 = OpVariable %2 Uniform"#, + ); + + let result = assemble_and_link(&[&a], &Options::default()); + let expect = r#"OpModuleProcessed "Linked by rspirv-linker" + %1 = OpTypeFloat 32 + %2 = OpVariable %1 Uniform"#; + without_header_eq(result, expect); + } + + #[test] + fn lib_extra_exports() { + let a = assemble_spirv( + r#"OpCapability Linkage + OpDecorate %1 LinkageAttributes "foo" Export + %2 = OpTypeFloat 32 + %1 = OpVariable %2 Uniform"#, + ); + + let result = assemble_and_link( + &[&a], + &Options { + lib: true, + ..Default::default() + }, + ); + + let expect = r#"OpModuleProcessed "Linked by rspirv-linker" + OpDecorate %1 LinkageAttributes "foo" Export + %2 = OpTypeFloat 32 + %1 = OpVariable %2 Uniform"#; + without_header_eq(result, expect); + } +}