├── tests └── data │ ├── .gitignore │ ├── for-loop.wgsl │ ├── for-loop.vert.glsl │ ├── debug-printf.vert.glsl │ ├── basic.frag.glsl │ └── for-loop.wgsl.spvasm ├── .gitmodules ├── .gitignore ├── .github ├── CODEOWNERS └── workflows │ ├── check-examples.sh │ ├── rustdoc-pages.yml │ └── ci.yml ├── rustfmt.toml ├── release.toml ├── LICENSE-MIT ├── Cargo.toml ├── examples ├── spv-lower-print.rs ├── spv-read-write-roundtrip.rs ├── spv-lower-lift-roundtrip.rs ├── spv-lower-link-lift.rs └── spv-lower-link-qptr-lift.rs ├── deny.toml ├── src ├── passes │ ├── legalize.rs │ ├── qptr.rs │ └── link.rs ├── qptr │ ├── shapes.rs │ └── mod.rs ├── spv │ ├── mod.rs │ ├── write.rs │ ├── print.rs │ └── read.rs ├── func_at.rs ├── visit.rs └── print │ └── multiversion.rs ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── CHANGELOG.md ├── README.md └── LICENSE-APACHE /tests/data/.gitignore: -------------------------------------------------------------------------------- 1 | *.spv 2 | *.spv.* 3 | *.spirt* 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "khronos-spec/SPIRV-Headers"] 2 | path = khronos-spec/SPIRV-Headers 3 | url = https://github.com/KhronosGroup/SPIRV-Headers 4 | branch = vulkan-sdk-1.3.275 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | 4 | .vscode/ 5 | 6 | # Profiling output files. 7 | callgrind.out.* 8 | perf.data* 9 | 10 | # HACK(eddyb) this is only for easily marking local-only files. 11 | \#* 12 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Documentation for this file can be found on the GitHub website here: 2 | # https://docs.github.com/en/free-pro-team@latest/github/creating-cloning-and-archiving-repositories/about-code-owners 3 | 4 | * @eddyb 5 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # HACK(eddyb) needed to format array/slice patterns at all, because it was a 2 | # breaking change (see https://github.com/rust-lang/rustfmt/pull/4994). 3 | version = "Two" 4 | 5 | # HACK(eddyb) avoid random spilling of e.g. method call chains onto many lines. 6 | use_small_heuristics = "Max" 7 | -------------------------------------------------------------------------------- /tests/data/for-loop.wgsl: -------------------------------------------------------------------------------- 1 | // Simple control-flow example (for `README.md`). 2 | // NOTE(eddyb) should be equivalent to `for-loop.vert.glsl`. 3 | 4 | @vertex 5 | fn main() -> @location(0) i32 { 6 | var o: i32 = 1; 7 | for(var i: i32 = 1; i < 10; i++) { 8 | o *= i; 9 | } 10 | return o; 11 | } 12 | -------------------------------------------------------------------------------- /tests/data/for-loop.vert.glsl: -------------------------------------------------------------------------------- 1 | // Simple control-flow example (for `README.md`). 2 | // NOTE(eddyb) should be equivalent to `for-loop.wgsl`. 3 | 4 | #version 450 5 | 6 | layout(location = 0) out int output0; 7 | 8 | void main() { 9 | int o = 1; 10 | for(int i = 1; i < 10; i++) 11 | o *= i; 12 | output0 = o; 13 | } 14 | -------------------------------------------------------------------------------- /tests/data/debug-printf.vert.glsl: -------------------------------------------------------------------------------- 1 | // `debugPrintfEXT`-using vertex shader, for use with `glslangValidator`, e.g.: 2 | // `glslangValidator -V --target-env spirv1.3 debug-printf.vert.glsl -o debug-printf.vert.glsl.spv` 3 | 4 | #version 450 5 | 6 | #extension GL_EXT_debug_printf : enable 7 | 8 | void main() { 9 | debugPrintfEXT("int=%u float=%f", 123, 123.456); 10 | } 11 | -------------------------------------------------------------------------------- /tests/data/basic.frag.glsl: -------------------------------------------------------------------------------- 1 | // Minimal fragment shader, for use with `glslangValidator`, e.g. full command 2 | // for a debuginfo build (the `--target-env` is to get `OpModuleProcessed`): 3 | // `glslangValidator -V --target-env spirv1.3 -g basic.frag.glsl -o basic.frag.glsl.dbg.spv` 4 | 5 | #version 450 6 | 7 | // NOTE(eddyb) some arbitrary extension for `OpSourceExtension`. 8 | #extension GL_EXT_scalar_block_layout : enable 9 | 10 | layout(location = 0) out vec4 output_color; 11 | 12 | void main() { 13 | output_color = vec4(1.0); 14 | } 15 | -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | pre-release-commit-message = "Release {{version}}" 2 | tag-message = "Release {{version}}" 3 | tag-name = "{{version}}" 4 | pre-release-replacements = [ 5 | { file = "Cargo.toml", prerelease = true, search = "repository = \"https://github.com/EmbarkStudios/spirt\"", replace = "repository = \"https://github.com/EmbarkStudios/spirt/tree/{{tag_name}}\"" }, 6 | { file = "CHANGELOG.md", search = "Unreleased", replace = "{{version}}" }, 7 | { file = "CHANGELOG.md", search = "\\.\\.\\.HEAD", replace = "...{{tag_name}}" }, 8 | { file = "CHANGELOG.md", search = "ReleaseDate", replace = "{{date}}" }, 9 | { file = "CHANGELOG.md", search = "", replace = "\n\n## [Unreleased] - ReleaseDate" }, 10 | { file = "CHANGELOG.md", search = "", replace = "\n[Unreleased]: https://github.com/EmbarkStudios/spirt/compare/{{tag_name}}...HEAD" }, 11 | ] 12 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Embark Studios 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "spirt" 3 | description = "Shader-focused IR to target, transform and translate from." 4 | repository = "https://github.com/EmbarkStudios/spirt" 5 | homepage = "https://github.com/EmbarkStudios/spirt" 6 | version = "0.3.0" 7 | authors = ["Embark "] 8 | edition = "2021" 9 | license = "MIT OR Apache-2.0" 10 | readme = "README.md" 11 | # FIXME(eddyb) should this point to the version built from `git`? 12 | documentation = "https://docs.rs/spirt" 13 | keywords = ["shader", "spir-v", "spirv", "ir", "compiler"] 14 | categories = [ 15 | "compilers", 16 | 17 | # FIXME(eddyb) `spirv-tools-sys` uses this but should it? 18 | "rendering::data-formats" 19 | ] 20 | exclude = [".github", "release.toml", "tests/data"] 21 | 22 | [dependencies] 23 | arrayvec = "0.7.1" 24 | bytemuck = "1.12.3" 25 | derive_more = "0.99.17" 26 | elsa = { version = "1.6.0", features = ["indexmap"] } 27 | indexmap = "2.0.0" 28 | internal-iterator = "0.2.0" 29 | itertools = "0.10.3" 30 | lazy_static = "1.4.0" 31 | longest-increasing-subsequence = "0.1.0" 32 | rustc-hash = "1.1.0" 33 | serde = { version = "1.0", features = ["derive"] } 34 | serde_json = "1.0" 35 | smallvec = { version = "1.7.0", features = ["serde", "union"] } 36 | 37 | [package.metadata.docs.rs] 38 | all-features = true 39 | rustdoc-args = ["--cfg", "docsrs", "--document-private-items"] 40 | -------------------------------------------------------------------------------- /.github/workflows/check-examples.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | # HACK(eddyb) `sed -E -z` expression to remove parts of the SPIR-T pretty-printing 5 | # output that we don't want to be actaully shown in an example (in `README.md`). 6 | spirt_cleanup_sed="\ 7 | s/module\.(dialect|debug_info) = spv\.Module(\.DebugInfo)?\(.*\)\n\n//g;\ 8 | s/\nexport \{\n( [^\n]*\n)*\}//\ 9 | " 10 | 11 | readme_examples=( 12 | tests/data/for-loop.wgsl.spvasm 13 | ) 14 | for example in "${readme_examples[@]}"; do 15 | # Examples are kept in SPIR-V textual assembly form, so they first have to 16 | # be converted to binary SPIR-V, to be used with any other tooling. 17 | spirv-as "$example" -o "$example.spv" 18 | 19 | # Pretty-print the SPIR-T we get by lowering (and restructuring) the SPIR-V. 20 | cargo run --release --example spv-lower-print "$example.spv" 21 | 22 | # FIXME(eddyb) perhaps support picking which SPIR-T output gets used? 23 | example_spirt="$example.structured.spirt" 24 | 25 | old="$( 26 | grep -Pazo \ 27 | "\n[\`]{3}.*\n\K(.*\n)*(?=[\`]{3}\n)" \ 28 | README.md \ 29 | | tr -d '\0' 30 | )" || (echo "README.md missing $example_spirt example!"; exit 1) 31 | new="$(sed -E -z "$spirt_cleanup_sed" < "$example_spirt")" 32 | diff -U3 <(echo "$old") <(echo "$new") || ( 33 | echo -e "\n\nREADME.md example out of date: $example\n" 34 | echo -e "=== old version (from README.md) ===\n$old\n" 35 | echo -e "=== new version ===\n$new\n" 36 | echo "(for more information, see $0 script)" 37 | exit 1 38 | ) 39 | done 40 | -------------------------------------------------------------------------------- /examples/spv-lower-print.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::path::Path; 3 | use std::rc::Rc; 4 | 5 | fn main() -> std::io::Result<()> { 6 | match &std::env::args().collect::>()[..] { 7 | [_, in_files @ ..] => { 8 | for in_file in in_files { 9 | let in_file_path = Path::new(in_file); 10 | 11 | let save_print_plan = |suffix: &str, plan: spirt::print::Plan| { 12 | let pretty = plan.pretty_print(); 13 | let ext = 14 | if suffix.is_empty() { "spirt".into() } else { format!("{suffix}.spirt") }; 15 | 16 | // FIXME(eddyb) don't allocate whole `String`s here. 17 | fs::write(in_file_path.with_extension(&ext), pretty.to_string())?; 18 | fs::write( 19 | in_file_path.with_extension(ext + ".html"), 20 | pretty.render_to_html().with_dark_mode_support().to_html_doc(), 21 | ) 22 | }; 23 | 24 | let mut module = spirt::Module::lower_from_spv_file( 25 | Rc::new(spirt::Context::new()), 26 | in_file_path, 27 | )?; 28 | save_print_plan("", spirt::print::Plan::for_module(&module))?; 29 | 30 | spirt::passes::legalize::structurize_func_cfgs(&mut module); 31 | save_print_plan("structured", spirt::print::Plan::for_module(&module))?; 32 | } 33 | 34 | Ok(()) 35 | } 36 | args => { 37 | eprintln!("Usage: {} FILES", args[0]); 38 | std::process::exit(1); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/spv-read-write-roundtrip.rs: -------------------------------------------------------------------------------- 1 | fn main() -> std::io::Result<()> { 2 | match &std::env::args().collect::>()[..] { 3 | [_, in_file, out_file] => { 4 | let parser = spirt::spv::read::ModuleParser::read_from_spv_file(in_file)?; 5 | let mut emitter = spirt::spv::write::ModuleEmitter::with_header(parser.header); 6 | 7 | { 8 | // FIXME(eddyb) show more of the header. 9 | let [_, v, ..] = parser.header; 10 | eprintln!("SPIR-V {}.{} module:", v >> 16, (v >> 8) & 0xff); 11 | } 12 | 13 | for inst in parser { 14 | let inst = inst.unwrap(); 15 | 16 | eprint!(" "); 17 | 18 | if let Some(id) = inst.result_id { 19 | eprint!("%{id}"); 20 | if let Some(type_id) = inst.result_type_id { 21 | eprint!(": %{type_id}"); 22 | } 23 | eprint!(" = "); 24 | } 25 | 26 | eprint!("{}", inst.opcode.name()); 27 | spirt::spv::print::inst_operands( 28 | inst.opcode, 29 | inst.imms.iter().copied(), 30 | inst.ids.iter().map(|id| format!("%{id}")), 31 | ) 32 | .for_each(|operand_parts| eprint!(" {}", operand_parts.concat_to_plain_text())); 33 | 34 | eprintln!(); 35 | 36 | emitter.push_inst(&inst).unwrap(); 37 | } 38 | 39 | emitter.write_to_spv_file(out_file) 40 | } 41 | args => { 42 | eprintln!("Usage: {} IN OUT", args[0]); 43 | std::process::exit(1); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/spv-lower-lift-roundtrip.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | fn main() -> std::io::Result<()> { 4 | match &std::env::args().collect::>()[..] { 5 | [_, in_file, out_file] => { 6 | let module = 7 | spirt::Module::lower_from_spv_file(Rc::new(spirt::Context::new()), in_file)?; 8 | module.lift_to_spv_file(out_file)?; 9 | 10 | // FIXME(eddyb) dump the module without reading the just-written file. 11 | let parser = spirt::spv::read::ModuleParser::read_from_spv_file(out_file)?; 12 | 13 | // FIXME(eddyb) deduplicate the rest of this function between this 14 | // example and `spv-read-write-roundtrip`. 15 | 16 | { 17 | // FIXME(eddyb) show more of the header. 18 | let [_, v, ..] = parser.header; 19 | eprintln!("SPIR-V {}.{} module:", v >> 16, (v >> 8) & 0xff); 20 | } 21 | 22 | for inst in parser { 23 | let inst = inst.unwrap(); 24 | 25 | eprint!(" "); 26 | 27 | if let Some(id) = inst.result_id { 28 | eprint!("%{id}"); 29 | if let Some(type_id) = inst.result_type_id { 30 | eprint!(": %{type_id}"); 31 | } 32 | eprint!(" = "); 33 | } 34 | 35 | eprint!("{}", inst.opcode.name()); 36 | spirt::spv::print::inst_operands( 37 | inst.opcode, 38 | inst.imms.iter().copied(), 39 | inst.ids.iter().map(|id| format!("%{id}")), 40 | ) 41 | .for_each(|operand_parts| eprint!(" {}", operand_parts.concat_to_plain_text())); 42 | 43 | eprintln!(); 44 | } 45 | 46 | Ok(()) 47 | } 48 | args => { 49 | eprintln!("Usage: {} IN OUT", args[0]); 50 | std::process::exit(1); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.github/workflows/rustdoc-pages.yml: -------------------------------------------------------------------------------- 1 | # Publishes docs built off latest git main branch to a GitHub Pages site. 2 | # The docs root will then be served at https://embarkstudios.github.io/spirt/spirt/index.html 3 | # 4 | # You must also go to the Pages settings for your repo and set it to serve from Actions for this to work 5 | name: Publish Docs 6 | 7 | on: 8 | workflow_dispatch: 9 | push: 10 | branches: [ "main" ] 11 | 12 | env: 13 | CARGO_TERM_COLOR: always 14 | 15 | jobs: 16 | build: 17 | name: Build Docs 18 | 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v3 24 | with: 25 | submodules: true 26 | # NOTE(eddyb) tags needed for `git describe --tags --always` below. 27 | fetch-depth: 0 28 | - name: Setup Rust Env 29 | uses: actions-rs/toolchain@v1 30 | with: 31 | toolchain: nightly 32 | override: true 33 | - name: Build Docs 34 | run: | 35 | RUSTDOCFLAGS="--cfg docs_build --cfg git_main_docs" \ 36 | GIT_MAIN_COMMIT="$(git rev-parse HEAD)" \ 37 | GIT_MAIN_DESCRIBE="$(git describe --tags --always)" \ 38 | cargo doc --document-private-items 39 | - name: Setup Pages 40 | id: pages 41 | uses: actions/configure-pages@v2 42 | - name: Upload artifact 43 | uses: actions/upload-pages-artifact@v1 44 | with: 45 | # Upload entire doc folder 46 | path: './target/doc' 47 | 48 | deploy: 49 | name: Deploy to Pages 50 | 51 | needs: build 52 | 53 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment 54 | permissions: 55 | pages: write # to deploy to Pages 56 | id-token: write # to verify the deployment originates from an appropriate source 57 | 58 | # Deploy to the github-pages environment 59 | environment: 60 | name: github-pages 61 | url: ${{ steps.deployment.outputs.page_url }} 62 | 63 | # Specify runner + deployment step 64 | runs-on: ubuntu-latest 65 | 66 | steps: 67 | - name: Deploy to GitHub Pages 68 | id: deployment 69 | uses: actions/deploy-pages@v1 70 | -------------------------------------------------------------------------------- /tests/data/for-loop.wgsl.spvasm: -------------------------------------------------------------------------------- 1 | ; Simple control-flow example (for `README.md`). 2 | ; 3 | ; NOTE(eddyb) while a version of this is in fact in `README.md`, the full thing 4 | ; couldn't fit, and variable names had to be shortened there (to reduce width). 5 | ; 6 | ; `for-loop.wgsl`, after being passed through: 7 | ; 1. `naga tests/data/for-loop.wgsl{,.spv}` 8 | ; 2. `spirv-opt --eliminate-local-multi-store --eliminate-dead-code-aggressive -o tests/data/for-loop.wgsl{.ssa,}.spv` 9 | ; 3. `spirv-dis tests/data/for-loop.wgsl.ssa.spv` 10 | ; 4. manual renaming, reformatting, commenting, etc. 11 | 12 | ; Metadata 13 | OpCapability Shader 14 | OpMemoryModel Logical GLSL450 15 | OpEntryPoint Vertex %main "main" %output0 16 | 17 | ; Decorations 18 | OpDecorate %output0 Location 0 19 | OpDecorate %output0 Flat 20 | 21 | ; Types 22 | %void = OpTypeVoid 23 | %bool = OpTypeBool 24 | %i32 = OpTypeInt 32 1 25 | 26 | ; Constants 27 | %1_i32 = OpConstant %i32 1 28 | %10_i32 = OpConstant %i32 10 29 | 30 | ; Global variables 31 | %typeof_output0 = OpTypePointer Output %i32 32 | %output0 = OpVariable %typeof_output0 Output 33 | 34 | ; Functions 35 | %typeof_main = OpTypeFunction %void 36 | %main = OpFunction %void None %typeof_main 37 | %entry = OpLabel 38 | OpBranch %bb_before_for 39 | 40 | %bb_before_for = OpLabel 41 | OpBranch %bb_for 42 | 43 | %bb_for = OpLabel 44 | %o = OpPhi %i32 %1_i32 %bb_before_for %o_next %bb_for_continue 45 | %i = OpPhi %i32 %1_i32 %bb_before_for %i_next %bb_for_continue 46 | OpLoopMerge %bb_after_for %bb_for_continue None 47 | OpBranch %bb_for_cond 48 | 49 | ; if !(i < 10) { break; } 50 | %bb_for_cond = OpLabel 51 | %cond = OpSLessThan %bool %i %10_i32 52 | OpSelectionMerge %bb_for_body None 53 | OpBranchConditional %cond %bb_for_body %bb_for_break 54 | 55 | %bb_for_break = OpLabel 56 | OpBranch %bb_after_for 57 | 58 | ; o *= i; 59 | %bb_for_body = OpLabel 60 | %o_next = OpIMul %i32 %o %i 61 | OpBranch %bb_for_continue 62 | 63 | ; i++ 64 | %bb_for_continue = OpLabel 65 | %i_next = OpIAdd %i32 %i %1_i32 66 | OpBranch %bb_for 67 | 68 | ; return o; 69 | %bb_after_for = OpLabel 70 | OpStore %output0 %o 71 | OpReturn 72 | OpFunctionEnd 73 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | # This section is considered when running `cargo deny check advisories` 2 | # More documentation for the advisories section can be found here: 3 | # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html 4 | [advisories] 5 | # The lint level for security vulnerabilities 6 | vulnerability = "deny" 7 | # The lint level for unmaintained crates 8 | unmaintained = "deny" 9 | # The lint level for crates that have been yanked from their source registry 10 | yanked = "deny" 11 | # The lint level for crates with security notices. Note that as of 12 | # 2019-12-17 there are no security notice advisories in 13 | # https://github.com/rustsec/advisory-db 14 | notice = "deny" 15 | 16 | # This section is considered when running `cargo deny check licenses` 17 | # More documentation for the licenses section can be found here: 18 | # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html 19 | [licenses] 20 | # The lint level for crates which do not have a detectable license 21 | unlicensed = "deny" 22 | # List of explicitly allowed licenses 23 | # See https://spdx.org/licenses/ for list of possible licenses 24 | # [possible values: any SPDX 3.11 short identifier (+ optional exception)]. 25 | allow = [ 26 | "MIT", 27 | "Apache-2.0", 28 | "Unicode-DFS-2016", 29 | ] 30 | # Lint level for licenses considered copyleft 31 | copyleft = "deny" 32 | 33 | # This section is considered when running `cargo deny check bans`. 34 | # More documentation about the 'bans' section can be found here: 35 | # https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html 36 | [bans] 37 | # Lint level for when multiple versions of the same crate are detected 38 | multiple-versions = "deny" 39 | # Lint level for when a crate version requirement is `*` 40 | wildcards = "deny" 41 | # Certain crates/versions that will be skipped when doing duplicate detection. 42 | skip = [ 43 | # FIXME(eddyb) `syn 2` has not replaced `syn 1` across the ecosystem yet. 44 | { name = "syn", version = "2" }, 45 | ] 46 | 47 | # This section is considered when running `cargo deny check sources`. 48 | # More documentation about the 'sources' section can be found here: 49 | # https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html 50 | [sources] 51 | # Lint level for what to happen when a crate from a crate registry that is not 52 | # in the allow list is encountered 53 | unknown-registry = "deny" 54 | # Lint level for what to happen when a crate from a git repository that is not 55 | # in the allow list is encountered 56 | unknown-git = "deny" 57 | -------------------------------------------------------------------------------- /src/passes/legalize.rs: -------------------------------------------------------------------------------- 1 | use crate::visit::{InnerVisit, Visitor}; 2 | use crate::{ 3 | cfg, AttrSet, Const, Context, DataInstForm, DeclDef, Func, FxIndexSet, GlobalVar, Module, Type, 4 | }; 5 | 6 | /// Apply the [`cfg::Structurizer`] algorithm to all function definitions in `module`. 7 | pub fn structurize_func_cfgs(module: &mut Module) { 8 | let cx = &module.cx(); 9 | 10 | // FIXME(eddyb) reuse this collection work in some kind of "pass manager". 11 | let mut collector = ReachableUseCollector { 12 | cx, 13 | module, 14 | 15 | seen_types: FxIndexSet::default(), 16 | seen_consts: FxIndexSet::default(), 17 | seen_data_inst_forms: FxIndexSet::default(), 18 | seen_global_vars: FxIndexSet::default(), 19 | seen_funcs: FxIndexSet::default(), 20 | }; 21 | for (export_key, &exportee) in &module.exports { 22 | export_key.inner_visit_with(&mut collector); 23 | exportee.inner_visit_with(&mut collector); 24 | } 25 | 26 | for &func in &collector.seen_funcs { 27 | if let DeclDef::Present(func_def_body) = &mut module.funcs[func].def { 28 | cfg::Structurizer::new(cx, func_def_body).structurize_func(); 29 | } 30 | } 31 | } 32 | 33 | struct ReachableUseCollector<'a> { 34 | cx: &'a Context, 35 | module: &'a Module, 36 | 37 | // FIXME(eddyb) build some automation to avoid ever repeating these. 38 | seen_types: FxIndexSet, 39 | seen_consts: FxIndexSet, 40 | seen_data_inst_forms: FxIndexSet, 41 | seen_global_vars: FxIndexSet, 42 | seen_funcs: FxIndexSet, 43 | } 44 | 45 | impl Visitor<'_> for ReachableUseCollector<'_> { 46 | // FIXME(eddyb) build some automation to avoid ever repeating these. 47 | fn visit_attr_set_use(&mut self, _attrs: AttrSet) { 48 | // FIXME(eddyb) if `AttrSet`s are ignored, why not `Type`s too? 49 | } 50 | fn visit_type_use(&mut self, ty: Type) { 51 | if self.seen_types.insert(ty) { 52 | self.visit_type_def(&self.cx[ty]); 53 | } 54 | } 55 | fn visit_const_use(&mut self, ct: Const) { 56 | if self.seen_consts.insert(ct) { 57 | self.visit_const_def(&self.cx[ct]); 58 | } 59 | } 60 | fn visit_data_inst_form_use(&mut self, data_inst_form: DataInstForm) { 61 | if self.seen_data_inst_forms.insert(data_inst_form) { 62 | self.visit_data_inst_form_def(&self.cx[data_inst_form]); 63 | } 64 | } 65 | 66 | fn visit_global_var_use(&mut self, gv: GlobalVar) { 67 | if self.seen_global_vars.insert(gv) { 68 | self.visit_global_var_decl(&self.module.global_vars[gv]); 69 | } 70 | } 71 | fn visit_func_use(&mut self, func: Func) { 72 | if self.seen_funcs.insert(func) { 73 | self.visit_func_decl(&self.module.funcs[func]); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at opensource@embark-studios.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | tags: 6 | - "*" 7 | pull_request: 8 | merge_group: 9 | 10 | name: CI 11 | jobs: 12 | lint: 13 | name: Lint 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | submodules: true 19 | - uses: actions-rs/toolchain@v1 20 | with: 21 | # HACK(eddyb) only nightly supports setting `version = "Two"` 22 | # in `rustfmt.toml`, which is needed to format array/slice patterns 23 | # at all (see https://github.com/rust-lang/rustfmt/pull/4994). 24 | toolchain: nightly 25 | override: true 26 | 27 | # run cargo fetch w/ --locked to verify Cargo.lock is up-to-date 28 | - run: cargo fetch --locked 29 | 30 | # make sure all code has been formatted with rustfmt 31 | - name: check rustfmt 32 | run: | 33 | rustup component add rustfmt 34 | cargo fmt -- --check --color always 35 | 36 | # run clippy to verify we have no warnings 37 | - name: cargo clippy 38 | run: | 39 | rustup component add clippy 40 | cargo clippy --all-targets --all-features -- -D warnings 41 | 42 | - name: Ensure compatibility with rust-gpu nightly rust version 43 | run: | 44 | curl -sSL https://github.com/EmbarkStudios/rust-gpu/raw/main/rust-toolchain.toml | grep -v '^components = ' > rust-toolchain.toml 45 | cargo check --workspace --all-targets 46 | rm rust-toolchain.toml 47 | 48 | test: 49 | name: Test 50 | strategy: 51 | matrix: 52 | os: [ubuntu-latest, windows-latest, macOS-latest] 53 | runs-on: ${{ matrix.os }} 54 | steps: 55 | - uses: actions/checkout@v3 56 | with: 57 | submodules: true 58 | - uses: actions-rs/toolchain@v1 59 | with: 60 | toolchain: stable 61 | override: true 62 | # NOTE(eddyb) this is the simplest way found so far to get `glslang`. 63 | - name: Prepare Vulkan SDK 64 | uses: humbletim/setup-vulkan-sdk@v1.2.0 65 | with: 66 | vulkan-query-version: 1.3.250.0 67 | vulkan-components: Glslang SPIRV-Tools 68 | vulkan-use-cache: true 69 | - run: cargo fetch 70 | # FIXME(eddyb) `cargo test` is not even that important yet. 71 | - name: cargo test build 72 | run: cargo build --tests --release --all-targets 73 | - name: cargo test 74 | run: cargo test --release --all-targets 75 | # FIXME(eddyb) actually flesh this out into a whole testing setup 76 | # (see also https://github.com/EmbarkStudios/spirt/issues/7). 77 | - name: Minimal glslang -> SPIR-V -> SPIR-T -> SPIR-V testing 78 | run: | 79 | glslangValidator -V --target-env spirv1.3 -g tests/data/basic.frag.glsl -o tests/data/basic.frag.glsl.dbg.spv 80 | glslangValidator -V --target-env spirv1.3 -g tests/data/debug-printf.vert.glsl -o tests/data/debug-printf.vert.glsl.dbg.spv 81 | cargo run --release --example spv-lower-link-lift tests/data/basic.frag.glsl.dbg.spv 82 | cargo run --release --example spv-lower-link-lift tests/data/debug-printf.vert.glsl.dbg.spv 83 | - if: ${{ runner.os == 'Linux' }} 84 | name: Check examples are up to date 85 | run: .github/workflows/check-examples.sh 86 | 87 | deny-check: 88 | name: cargo-deny 89 | runs-on: ubuntu-latest 90 | steps: 91 | - uses: actions/checkout@v3 92 | with: 93 | submodules: true 94 | - uses: EmbarkStudios/cargo-deny-action@v1 95 | 96 | publish-check: 97 | name: Publish Check 98 | runs-on: ubuntu-latest 99 | steps: 100 | - uses: actions/checkout@v3 101 | with: 102 | submodules: true 103 | - uses: actions-rs/toolchain@v1 104 | with: 105 | toolchain: stable 106 | override: true 107 | - run: cargo fetch 108 | - name: cargo publish check 109 | run: cargo publish --dry-run 110 | -------------------------------------------------------------------------------- /examples/spv-lower-link-lift.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::path::Path; 3 | use std::rc::Rc; 4 | 5 | fn main() -> std::io::Result<()> { 6 | match &std::env::args().collect::>()[..] { 7 | [_, in_file] => { 8 | let in_file_path = Path::new(in_file); 9 | 10 | let save_print_plan = |suffix: &str, plan: spirt::print::Plan| { 11 | let pretty = plan.pretty_print(); 12 | let ext = format!("{suffix}.spirt"); 13 | 14 | // FIXME(eddyb) don't allocate whole `String`s here. 15 | fs::write(in_file_path.with_extension(&ext), pretty.to_string())?; 16 | fs::write( 17 | in_file_path.with_extension(ext + ".html"), 18 | pretty.render_to_html().with_dark_mode_support().to_html_doc(), 19 | ) 20 | }; 21 | 22 | // FIXME(eddyb) adapt the other examples to this style. 23 | 24 | fn eprint_duration(f: impl FnOnce() -> R) -> R { 25 | let start = std::time::Instant::now(); 26 | let r = f(); 27 | eprint!("[{:8.3}ms] ", start.elapsed().as_secs_f64() * 1000.0); 28 | r 29 | } 30 | 31 | eprint_duration(|| { 32 | let _ = spirt::spv::spec::Spec::get(); 33 | }); 34 | eprintln!("spv::spec::Spec::get"); 35 | 36 | let cx = Rc::new(spirt::Context::new()); 37 | 38 | let multi_version_printing = true; 39 | let mut per_pass_module = vec![]; 40 | let mut after_pass = |pass, module: &spirt::Module| { 41 | if multi_version_printing { 42 | per_pass_module.push((pass, module.clone())); 43 | Ok(()) 44 | } else { 45 | save_print_plan( 46 | &format!("after.{pass}"), 47 | spirt::print::Plan::for_module(module), 48 | ) 49 | } 50 | }; 51 | 52 | let mut module = 53 | eprint_duration(|| spirt::Module::lower_from_spv_file(cx.clone(), in_file_path))?; 54 | eprintln!("Module::lower_from_spv_file({})", in_file_path.display()); 55 | 56 | let original_export_count = module.exports.len(); 57 | eprint_duration(|| { 58 | spirt::passes::link::minimize_exports(&mut module, |export_key| { 59 | matches!(export_key, spirt::ExportKey::SpvEntryPoint { .. }) 60 | }) 61 | }); 62 | eprintln!( 63 | "link::minimize_exports: {} -> {} exports", 64 | original_export_count, 65 | module.exports.len() 66 | ); 67 | after_pass("minimize_exports", &module)?; 68 | 69 | // HACK(eddyb) do this late enough to avoid spending time on unused 70 | // functions, which `link::minimize_exports` makes unreachable. 71 | eprint_duration(|| spirt::passes::legalize::structurize_func_cfgs(&mut module)); 72 | eprintln!("legalize::structurize_func_cfgs"); 73 | after_pass("structurize_func_cfgs", &module)?; 74 | 75 | eprint_duration(|| spirt::passes::link::resolve_imports(&mut module)); 76 | eprintln!("link::resolve_imports"); 77 | after_pass("resolve_imports", &module)?; 78 | 79 | if multi_version_printing { 80 | // FIXME(eddyb) use a better suffix than `link` (or none). 81 | save_print_plan( 82 | "link", 83 | spirt::print::Plan::for_versions( 84 | &cx, 85 | per_pass_module 86 | .iter() 87 | .map(|(pass, module)| (format!("after {pass}"), module)), 88 | ), 89 | )?; 90 | } 91 | 92 | let out_file_path = in_file_path.with_extension("link.spv"); 93 | eprint_duration(|| module.lift_to_spv_file(&out_file_path))?; 94 | eprintln!("Module::lift_to_spv_file({})", out_file_path.display()); 95 | 96 | Ok(()) 97 | } 98 | args => { 99 | eprintln!("Usage: {} IN", args[0]); 100 | std::process::exit(1); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Embark Contributor Guidelines 2 | 3 | Welcome! This project is created by the team at [Embark Studios](https://embark.games). We're glad you're interested in contributing! We welcome contributions from people of all backgrounds who are interested in making great software with us. 4 | 5 | At Embark, we aspire to empower everyone to create interactive experiences. To do this, we're exploring and pushing the boundaries of new technologies, and sharing our learnings with the open source community. 6 | 7 | If you have ideas for collaboration, email us at opensource@embark-studios.com. 8 | 9 | We're also hiring full-time engineers to work with us in Stockholm! Check out our current job postings [here](https://www.embark-studios.com/jobs). 10 | 11 | ## Issues 12 | 13 | ### Feature Requests 14 | 15 | If you have ideas or how to improve our projects, you can suggest features by opening a GitHub issue. Make sure to include details about the feature or change, and describe any uses cases it would enable. 16 | 17 | Feature requests will be tagged as `enhancement` and their status will be updated in the comments of the issue. 18 | 19 | ### Bugs 20 | 21 | When reporting a bug or unexpected behavior in a project, make sure your issue describes steps to reproduce the behavior, including the platform you were using, what steps you took, and any error messages. 22 | 23 | Reproducible bugs will be tagged as `bug` and their status will be updated in the comments of the issue. 24 | 25 | ### Wontfix 26 | 27 | Issues will be closed and tagged as `wontfix` if we decide that we do not wish to implement it, usually due to being misaligned with the project vision or out of scope. We will comment on the issue with more detailed reasoning. 28 | 29 | ## Contribution Workflow 30 | 31 | ### Open Issues 32 | 33 | If you're ready to contribute, start by looking at our open issues tagged as [`help wanted`](../../issues?q=is%3Aopen+is%3Aissue+label%3A"help+wanted") or [`good first issue`](../../issues?q=is%3Aopen+is%3Aissue+label%3A"good+first+issue"). 34 | 35 | You can comment on the issue to let others know you're interested in working on it or to ask questions. 36 | 37 | ### Making Changes 38 | 39 | 1. Fork the repository. 40 | 41 | 2. Create a new feature branch. 42 | 43 | 3. Make your changes. Ensure that there are no build errors by running the project with your changes locally. 44 | 45 | 4. Open a pull request with a name and description of what you did. You can read more about working with pull requests on GitHub [here](https://help.github.com/en/articles/creating-a-pull-request-from-a-fork). 46 | 47 | 5. A maintainer will review your pull request and may ask you to make changes. 48 | 49 | ## Code Guidelines 50 | 51 | ### Rust 52 | 53 | You can read about our standards and recommendations for working with Rust [here](https://github.com/EmbarkStudios/rust-ecosystem/blob/main/guidelines.md). 54 | 55 | ### Python 56 | 57 | We recommend following [PEP8 conventions](https://www.python.org/dev/peps/pep-0008/) when working with Python modules. 58 | 59 | ### JavaScript & TypeScript 60 | 61 | We use [Prettier](https://prettier.io/) with the default settings to auto-format our JavaScript and TypeScript code. 62 | 63 | ## Licensing 64 | 65 | Unless otherwise specified, all Embark open source projects shall comply with the Rust standard licensing model (MIT + Apache 2.0) and are thereby licensed under a dual license, allowing licensees to choose either MIT OR Apache-2.0 at their option. 66 | 67 | ## Contributor Terms 68 | 69 | Thank you for your interest in Embark Studios’ open source project. By providing a contribution (new or modified code, other input, feedback or suggestions etc.) you agree to these Contributor Terms. 70 | 71 | You confirm that each of your contributions has been created by you and that you are the copyright owner. You also confirm that you have the right to provide the contribution to us and that you do it under the Rust dual licence model (MIT + Apache 2.0). 72 | 73 | If you want to contribute something that is not your original creation, you may submit it to Embark Studios separately from any contribution, including details of its source and of any license or other restriction (such as related patents, trademarks, agreements etc.) 74 | 75 | Please also note that our projects are released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md) to ensure that they are welcoming places for everyone to contribute. By participating in any Embark Studios open source project, you agree to keep to the Contributor Code of Conduct. 76 | -------------------------------------------------------------------------------- /src/passes/qptr.rs: -------------------------------------------------------------------------------- 1 | //! [`QPtr`](crate::TypeKind::QPtr) transforms. 2 | 3 | use crate::visit::{InnerVisit, Visitor}; 4 | use crate::{qptr, DataInstForm}; 5 | use crate::{AttrSet, Const, Context, Func, FxIndexSet, GlobalVar, Module, Type}; 6 | 7 | pub fn lower_from_spv_ptrs(module: &mut Module, layout_config: &qptr::LayoutConfig) { 8 | let cx = &module.cx(); 9 | 10 | let (seen_global_vars, seen_funcs) = { 11 | // FIXME(eddyb) reuse this collection work in some kind of "pass manager". 12 | let mut collector = ReachableUseCollector { 13 | cx, 14 | module, 15 | 16 | seen_types: FxIndexSet::default(), 17 | seen_consts: FxIndexSet::default(), 18 | seen_data_inst_forms: FxIndexSet::default(), 19 | seen_global_vars: FxIndexSet::default(), 20 | seen_funcs: FxIndexSet::default(), 21 | }; 22 | for (export_key, &exportee) in &module.exports { 23 | export_key.inner_visit_with(&mut collector); 24 | exportee.inner_visit_with(&mut collector); 25 | } 26 | (collector.seen_global_vars, collector.seen_funcs) 27 | }; 28 | 29 | let lowerer = qptr::lower::LowerFromSpvPtrs::new(cx.clone(), layout_config); 30 | for &global_var in &seen_global_vars { 31 | lowerer.lower_global_var(&mut module.global_vars[global_var]); 32 | } 33 | for &func in &seen_funcs { 34 | lowerer.lower_func(&mut module.funcs[func]); 35 | } 36 | } 37 | 38 | pub fn analyze_uses(module: &mut Module, layout_config: &qptr::LayoutConfig) { 39 | qptr::analyze::InferUsage::new(module.cx(), layout_config).infer_usage_in_module(module); 40 | } 41 | 42 | pub fn lift_to_spv_ptrs(module: &mut Module, layout_config: &qptr::LayoutConfig) { 43 | let cx = &module.cx(); 44 | 45 | let (seen_global_vars, seen_funcs) = { 46 | // FIXME(eddyb) reuse this collection work in some kind of "pass manager". 47 | let mut collector = ReachableUseCollector { 48 | cx, 49 | module, 50 | 51 | seen_types: FxIndexSet::default(), 52 | seen_consts: FxIndexSet::default(), 53 | seen_data_inst_forms: FxIndexSet::default(), 54 | seen_global_vars: FxIndexSet::default(), 55 | seen_funcs: FxIndexSet::default(), 56 | }; 57 | for (export_key, &exportee) in &module.exports { 58 | export_key.inner_visit_with(&mut collector); 59 | exportee.inner_visit_with(&mut collector); 60 | } 61 | (collector.seen_global_vars, collector.seen_funcs) 62 | }; 63 | 64 | let lifter = qptr::lift::LiftToSpvPtrs::new(cx.clone(), layout_config); 65 | for &global_var in &seen_global_vars { 66 | lifter.lift_global_var(&mut module.global_vars[global_var]); 67 | } 68 | lifter.lift_all_funcs(module, seen_funcs); 69 | } 70 | 71 | struct ReachableUseCollector<'a> { 72 | cx: &'a Context, 73 | module: &'a Module, 74 | 75 | // FIXME(eddyb) build some automation to avoid ever repeating these. 76 | seen_types: FxIndexSet, 77 | seen_consts: FxIndexSet, 78 | seen_data_inst_forms: FxIndexSet, 79 | seen_global_vars: FxIndexSet, 80 | seen_funcs: FxIndexSet, 81 | } 82 | 83 | impl Visitor<'_> for ReachableUseCollector<'_> { 84 | // FIXME(eddyb) build some automation to avoid ever repeating these. 85 | fn visit_attr_set_use(&mut self, _attrs: AttrSet) { 86 | // FIXME(eddyb) if `AttrSet`s are ignored, why not `Type`s too? 87 | } 88 | fn visit_type_use(&mut self, ty: Type) { 89 | if self.seen_types.insert(ty) { 90 | self.visit_type_def(&self.cx[ty]); 91 | } 92 | } 93 | fn visit_const_use(&mut self, ct: Const) { 94 | if self.seen_consts.insert(ct) { 95 | self.visit_const_def(&self.cx[ct]); 96 | } 97 | } 98 | fn visit_data_inst_form_use(&mut self, data_inst_form: DataInstForm) { 99 | if self.seen_data_inst_forms.insert(data_inst_form) { 100 | self.visit_data_inst_form_def(&self.cx[data_inst_form]); 101 | } 102 | } 103 | 104 | fn visit_global_var_use(&mut self, gv: GlobalVar) { 105 | if self.seen_global_vars.insert(gv) { 106 | self.visit_global_var_decl(&self.module.global_vars[gv]); 107 | } 108 | } 109 | fn visit_func_use(&mut self, func: Func) { 110 | if self.seen_funcs.insert(func) { 111 | self.visit_func_decl(&self.module.funcs[func]); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/qptr/shapes.rs: -------------------------------------------------------------------------------- 1 | //! Variable shapes (untyped memory layouts vs abstract resources). 2 | // 3 | // FIXME(eddyb) does this need its own module still? 4 | 5 | use crate::{AddrSpace, Type}; 6 | use std::num::NonZeroU32; 7 | 8 | /// `GlobalVar`s are currently used for both chunks of plain data (i.e. memory), 9 | /// and the "shader interface" (inherited by `Shader` SPIR-V from GLSL, whereas 10 | /// `Kernel` SPIR-V ended up with `OpenCL`'s "resources are passed to entry-points 11 | /// as regular function arguments", with `BuiltIn`+`Input` as a sole exception). 12 | #[derive(Copy, Clone, PartialEq, Eq)] 13 | pub enum GlobalVarShape { 14 | /// One or more (i.e. optionally arrayed) "abstract resource" `Handle`s 15 | /// (see `Handle` documentation for more on what it can represent). 16 | /// 17 | /// The single handle case is equivalent to a length `1` array of handles, 18 | /// and as such is represented by having `fixed_count` be `Some(1)`. 19 | Handles { 20 | handle: Handle, 21 | fixed_count: Option, 22 | }, 23 | 24 | // FIXME(eddyb) unify terminology around "concrete"/"memory"/"untyped (data)". 25 | UntypedData(MemLayout), 26 | 27 | /// Non-memory pipeline interface, which must keep the exact original type, 28 | /// even if that type is concrete and could be handled just like memory. 29 | /// 30 | /// Typically `Input` or `Output`, but extensions (e.g. ray-tracing) may add 31 | /// more such interface storage classes with strict type requirements. 32 | // 33 | // FIXME(eddyb) consider replacing this with by-value entry-point args/return 34 | // (though that would not solve some of the weirder ones). 35 | TypedInterface(Type), 36 | } 37 | 38 | /// "Abstract resource" handle, that can be found in non-memory `GlobalVar`s. 39 | /// 40 | /// This largely corresponds to the Vulkan concept of a "descriptor", and arrays 41 | /// of handles (e.g. `GlobalVarShape::Handles` with `fixed_count != Some(1)`) 42 | /// map to the "descriptor indexing" usecase. 43 | // 44 | // FIXME(eddyb) consider implementing "descriptor indexing" more like HLSL's 45 | // "resource heap" (with types only specified at use sites, "casts" almost). 46 | #[derive(Copy, Clone, PartialEq, Eq, Hash)] 47 | pub enum Handle { 48 | /// Fully opaque resources (e.g. samplers, images). 49 | Opaque(Type), 50 | 51 | /// Buffer resources, describing ranges of (technically) untyped memory in 52 | /// some address space (e.g. `Uniform`, `StorageBuffer`), but being limited 53 | /// by SPIR-V logical addressing (unlike e.g. `PhysicalStorageBuffer`). 54 | /// 55 | /// SPIR-V makes this particularly painful, through a couple of design flaws: 56 | /// - forcing a static type (for the buffer contents) and disallowing any 57 | /// pointer casts, despite the fact that any plausible representation for 58 | /// "logical pointer into a buffer" (e.g. `(BufferDescriptor, Offset)`) 59 | /// must be *fundamentally* untyped (as it must allow access to relatively 60 | /// large amounts of memory, and also support dynamic array indexing), 61 | /// even when not a "GPU memory address" (like `PhysicalStorageBuffer`) 62 | /// - encoding the buffer type using a (GLSL-style) "interface block", where 63 | /// instead of a special type (or a pointer with the right storage class), 64 | /// an `OpTypeStruct` (having the statically typed buffer contents as fields) 65 | /// with the `Block` decoration is used, and then this "interface block" 66 | /// type can be further nested in `OpTypeArray` or `OpTypeRuntimeArray` 67 | /// to allow descriptor indexing - which leads to constructs like a GLSL 68 | /// `buffer { uint data[]; } bufs[];` being encoded with two levels of 69 | /// `OpTypeRuntimeArray`, separated not by any explicit indirection, but 70 | /// only by the `Block` decoration on the `OpTypeStruct` for `buffer {...}` 71 | // 72 | // FIXME(eddyb) should `PushConstant` use `GlobalVarShape::UntypedData` 73 | // instead of being treated like a buffer? 74 | // 75 | // FIXME(eddyb) should this be a `Type` of its own, that can be loaded from 76 | // a handle `QPtr`, and then has data pointer / length ops *on that*? 77 | Buffer(AddrSpace, BL), 78 | } 79 | 80 | /// Untyped memory shape with constant alignment and size. 81 | /// 82 | /// `align`/`legacy_align` correspond to "scalar"/"base" alignments in Vulkan, 83 | /// and are both kept track of to detect ambiguity in implicit layouts, e.g. 84 | /// field offsets when the `Offset` decoration isn't being used. 85 | /// Note, however, that `legacy_align` can be raised to "extended" alignment, 86 | /// or completeley ignored, using [`LayoutConfig`](crate::qptr::LayoutConfig). 87 | /// 88 | /// Only `align` is *required*, that is `size % align == 0` must be always enforced. 89 | // 90 | // FIXME(eddyb) consider supporting specialization-constant-length arrays. 91 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] 92 | pub struct MemLayout { 93 | // FIXME(eddyb) use proper newtypes (and log2 for align!). 94 | pub align: u32, 95 | pub legacy_align: u32, 96 | pub size: u32, 97 | } 98 | 99 | /// Untyped memory shape with constant alignment but potentially-dynamic size, 100 | /// roughly corresponding to a Rust `(FixedBase, [DynUnit])` type's layout. 101 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 102 | pub struct MaybeDynMemLayout { 103 | pub fixed_base: MemLayout, 104 | pub dyn_unit_stride: Option, 105 | } 106 | -------------------------------------------------------------------------------- /examples/spv-lower-link-qptr-lift.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::path::Path; 3 | use std::rc::Rc; 4 | 5 | fn main() -> std::io::Result<()> { 6 | match &std::env::args().collect::>()[..] { 7 | [_, in_file] => { 8 | let in_file_path = Path::new(in_file); 9 | 10 | let save_print_plan = |suffix: &str, plan: spirt::print::Plan| { 11 | let pretty = plan.pretty_print(); 12 | let ext = format!("{suffix}.spirt"); 13 | 14 | // FIXME(eddyb) don't allocate whole `String`s here. 15 | fs::write(in_file_path.with_extension(&ext), pretty.to_string())?; 16 | fs::write( 17 | in_file_path.with_extension(ext + ".html"), 18 | pretty.render_to_html().with_dark_mode_support().to_html_doc(), 19 | ) 20 | }; 21 | 22 | // FIXME(eddyb) adapt the other examples to this style. 23 | 24 | fn eprint_duration(f: impl FnOnce() -> R) -> R { 25 | let start = std::time::Instant::now(); 26 | let r = f(); 27 | eprint!("[{:8.3}ms] ", start.elapsed().as_secs_f64() * 1000.0); 28 | r 29 | } 30 | 31 | eprint_duration(|| { 32 | let _ = spirt::spv::spec::Spec::get(); 33 | }); 34 | eprintln!("spv::spec::Spec::get"); 35 | 36 | let cx = Rc::new(spirt::Context::new()); 37 | 38 | let multi_version_printing = true; 39 | let mut per_pass_module = vec![]; 40 | let mut after_pass = |pass, module: &spirt::Module| { 41 | if multi_version_printing { 42 | per_pass_module.push((pass, module.clone())); 43 | Ok(()) 44 | } else { 45 | save_print_plan( 46 | &format!("after.{pass}"), 47 | spirt::print::Plan::for_module(module), 48 | ) 49 | } 50 | }; 51 | 52 | let mut module = 53 | eprint_duration(|| spirt::Module::lower_from_spv_file(cx.clone(), in_file_path))?; 54 | eprintln!("Module::lower_from_spv_file({})", in_file_path.display()); 55 | 56 | let original_export_count = module.exports.len(); 57 | eprint_duration(|| { 58 | spirt::passes::link::minimize_exports(&mut module, |export_key| { 59 | matches!(export_key, spirt::ExportKey::SpvEntryPoint { .. }) 60 | }) 61 | }); 62 | eprintln!( 63 | "link::minimize_exports: {} -> {} exports", 64 | original_export_count, 65 | module.exports.len() 66 | ); 67 | //after_pass("minimize_exports", &module)?; 68 | 69 | // HACK(eddyb) do this late enough to avoid spending time on unused 70 | // functions, which `link::minimize_exports` makes unreachable. 71 | eprint_duration(|| spirt::passes::legalize::structurize_func_cfgs(&mut module)); 72 | eprintln!("legalize::structurize_func_cfgs"); 73 | //after_pass("structurize_func_cfgs", &module)?; 74 | 75 | eprint_duration(|| spirt::passes::link::resolve_imports(&mut module)); 76 | eprintln!("link::resolve_imports"); 77 | //after_pass("resolve_imports", &module)?; 78 | 79 | // HACK(eddyb) 80 | after_pass("", &module)?; 81 | 82 | // HACK(eddyb) this is roughly what Rust-GPU would need. 83 | let layout_config = &spirt::qptr::LayoutConfig { 84 | abstract_bool_size_align: (1, 1), 85 | logical_ptr_size_align: (4, 4), 86 | ..spirt::qptr::LayoutConfig::VULKAN_SCALAR_LAYOUT 87 | }; 88 | 89 | eprint_duration(|| { 90 | spirt::passes::qptr::lower_from_spv_ptrs(&mut module, layout_config) 91 | }); 92 | eprintln!("qptr::lower_from_spv_ptrs"); 93 | after_pass("qptr::lower_from_spv_ptrs", &module)?; 94 | 95 | eprint_duration(|| spirt::passes::qptr::analyze_uses(&mut module, layout_config)); 96 | eprintln!("qptr::analyze_uses"); 97 | after_pass("qptr::analyze_uses", &module)?; 98 | 99 | eprint_duration(|| spirt::passes::qptr::lift_to_spv_ptrs(&mut module, layout_config)); 100 | eprintln!("qptr::lift_to_spv_ptrs"); 101 | after_pass("qptr::lift_to_spv_ptrs", &module)?; 102 | 103 | if multi_version_printing { 104 | // FIXME(eddyb) use a better suffix than `qptr` (or none). 105 | save_print_plan( 106 | "qptr", 107 | spirt::print::Plan::for_versions( 108 | &cx, 109 | per_pass_module.iter().map(|(pass, module)| { 110 | ( 111 | // HACK(eddyb) 112 | if pass.is_empty() { 113 | "initial".into() 114 | } else { 115 | format!("after {pass}") 116 | }, 117 | module, 118 | ) 119 | }), 120 | ), 121 | )?; 122 | } 123 | 124 | //let out_file_path = in_file_path.with_extension("qptr.spv"); 125 | //eprint_duration(|| module.lift_to_spv_file(&out_file_path))?; 126 | //eprintln!("Module::lift_to_spv_file({})", out_file_path.display()); 127 | 128 | Ok(()) 129 | } 130 | args => { 131 | eprintln!("Usage: {} IN", args[0]); 132 | std::process::exit(1); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/spv/mod.rs: -------------------------------------------------------------------------------- 1 | //! SPIR-V support, mainly conversions to/from SPIR-T ([`lower`]/[`lift`]). 2 | 3 | // NOTE(eddyb) all the modules are declared here, but they're documented "inside" 4 | // (i.e. using inner doc comments). 5 | pub mod lift; 6 | pub mod lower; 7 | pub mod print; 8 | pub mod read; 9 | pub mod spec; 10 | pub mod write; 11 | 12 | use crate::{FxIndexMap, InternedStr}; 13 | use smallvec::SmallVec; 14 | use std::collections::{BTreeMap, BTreeSet}; 15 | use std::iter; 16 | use std::num::NonZeroU32; 17 | use std::string::FromUtf8Error; 18 | 19 | /// Semantic properties of a SPIR-V module (not tied to any IDs). 20 | #[derive(Clone)] 21 | pub struct Dialect { 22 | pub version_major: u8, 23 | pub version_minor: u8, 24 | 25 | pub capabilities: BTreeSet, 26 | pub extensions: BTreeSet, 27 | 28 | pub addressing_model: u32, 29 | pub memory_model: u32, 30 | } 31 | 32 | /// Non-semantic details (i.e. debuginfo) of a SPIR-V module (not tied to any IDs). 33 | #[derive(Clone)] 34 | pub struct ModuleDebugInfo { 35 | pub original_generator_magic: Option, 36 | 37 | pub source_languages: BTreeMap, 38 | pub source_extensions: Vec, 39 | pub module_processes: Vec, 40 | } 41 | 42 | #[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] 43 | pub struct DebugSourceLang { 44 | pub lang: u32, 45 | pub version: u32, 46 | } 47 | 48 | #[derive(Clone, Default)] 49 | pub struct DebugSources { 50 | pub file_contents: FxIndexMap, 51 | } 52 | 53 | /// A SPIR-V instruction, in its minimal form (opcode and immediate operands). 54 | #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 55 | pub struct Inst { 56 | pub opcode: spec::Opcode, 57 | 58 | // FIXME(eddyb) change the inline size of this to fit most instructions. 59 | // FIXME(eddyb) it might be worth investigating the performance implications 60 | // of interning "long immediates", compared to the flattened representation. 61 | // NOTE(eddyb) interning these separately is likely unnecessary in many cases, 62 | // now that `DataInstForm`s are interned, and `Const`s etc. were already. 63 | pub imms: SmallVec<[Imm; 2]>, 64 | } 65 | 66 | impl From for Inst { 67 | fn from(opcode: spec::Opcode) -> Self { 68 | Self { opcode, imms: SmallVec::new() } 69 | } 70 | } 71 | 72 | /// A full SPIR-V instruction (like [`Inst`], but including input/output ID operands). 73 | pub struct InstWithIds { 74 | pub without_ids: Inst, 75 | 76 | // FIXME(eddyb) consider nesting "Result Type ID" in "Result ID". 77 | pub result_type_id: Option, 78 | pub result_id: Option, 79 | 80 | // FIXME(eddyb) change the inline size of this to fit most instructions. 81 | pub ids: SmallVec<[Id; 4]>, 82 | } 83 | 84 | // HACK(eddyb) access to `Inst` fields for convenience. 85 | impl std::ops::Deref for InstWithIds { 86 | type Target = Inst; 87 | fn deref(&self) -> &Inst { 88 | &self.without_ids 89 | } 90 | } 91 | impl std::ops::DerefMut for InstWithIds { 92 | fn deref_mut(&mut self) -> &mut Inst { 93 | &mut self.without_ids 94 | } 95 | } 96 | 97 | /// SPIR-V immediate (one word, longer immediates are a sequence of multiple [`Imm`]s). 98 | // 99 | // FIXME(eddyb) consider replacing with a `struct` e.g.: 100 | // `{ first: bool, last: bool, kind: OperandKind, word: u32 }` 101 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 102 | pub enum Imm { 103 | Short(spec::OperandKind, u32), 104 | LongStart(spec::OperandKind, u32), 105 | LongCont(spec::OperandKind, u32), 106 | } 107 | 108 | /// SPIR-V ID. 109 | pub type Id = NonZeroU32; 110 | 111 | // FIXME(eddyb) pick a "small string" crate, and fine-tune its inline size, 112 | // instead of allocating a whole `String`. 113 | // 114 | /// Given a single `LiteralString` (as one [`Imm::Short`] or a [`Imm::LongStart`] 115 | /// followed by some number of [`Imm::LongCont`] - will panic otherwise), returns a 116 | /// Rust [`String`] if the literal is valid UTF-8, or the validation error otherwise. 117 | pub fn extract_literal_string(imms: &[Imm]) -> Result { 118 | let wk = &spec::Spec::get().well_known; 119 | 120 | let mut words = match *imms { 121 | [Imm::Short(kind, first_word)] | [Imm::LongStart(kind, first_word), ..] => { 122 | assert_eq!(kind, wk.LiteralString); 123 | iter::once(first_word).chain(imms[1..].iter().map(|&imm| match imm { 124 | Imm::LongCont(kind, word) => { 125 | assert_eq!(kind, wk.LiteralString); 126 | word 127 | } 128 | _ => unreachable!(), 129 | })) 130 | } 131 | _ => unreachable!(), 132 | }; 133 | 134 | let mut bytes = Vec::with_capacity(imms.len() * 4); 135 | while let Some(word) = words.next() { 136 | for byte in word.to_le_bytes() { 137 | if byte == 0 { 138 | assert!(words.next().is_none()); 139 | return String::from_utf8(bytes); 140 | } 141 | bytes.push(byte); 142 | } 143 | } 144 | unreachable!("missing \\0 terminator in LiteralString"); 145 | } 146 | 147 | // FIXME(eddyb) this shouldn't just panic when `s.contains('\0')`. 148 | pub fn encode_literal_string(s: &str) -> impl Iterator + '_ { 149 | let wk = &spec::Spec::get().well_known; 150 | 151 | let bytes = s.as_bytes(); 152 | 153 | // FIXME(eddyb) replace with `array_chunks` once that is stabilized. 154 | let full_words = bytes.chunks_exact(4).map(|w| <[u8; 4]>::try_from(w).unwrap()); 155 | 156 | let leftover_bytes = &bytes[full_words.len() * 4..]; 157 | let mut last_word = [0; 4]; 158 | last_word[..leftover_bytes.len()].copy_from_slice(leftover_bytes); 159 | 160 | let total_words = full_words.len() + 1; 161 | 162 | full_words.chain(iter::once(last_word)).map(u32::from_le_bytes).enumerate().map( 163 | move |(i, word)| { 164 | let kind = wk.LiteralString; 165 | match (i, total_words) { 166 | (0, 1) => Imm::Short(kind, word), 167 | (0, _) => Imm::LongStart(kind, word), 168 | (_, _) => Imm::LongCont(kind, word), 169 | } 170 | }, 171 | ) 172 | } 173 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "arrayvec" 7 | version = "0.7.4" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" 10 | 11 | [[package]] 12 | name = "bytemuck" 13 | version = "1.14.1" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "ed2490600f404f2b94c167e31d3ed1d5f3c225a0f3b80230053b3e0b7b962bd9" 16 | 17 | [[package]] 18 | name = "convert_case" 19 | version = "0.4.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" 22 | 23 | [[package]] 24 | name = "derive_more" 25 | version = "0.99.17" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" 28 | dependencies = [ 29 | "convert_case", 30 | "proc-macro2", 31 | "quote", 32 | "rustc_version", 33 | "syn 1.0.109", 34 | ] 35 | 36 | [[package]] 37 | name = "either" 38 | version = "1.9.0" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" 41 | 42 | [[package]] 43 | name = "elsa" 44 | version = "1.10.0" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "d98e71ae4df57d214182a2e5cb90230c0192c6ddfcaa05c36453d46a54713e10" 47 | dependencies = [ 48 | "indexmap", 49 | "stable_deref_trait", 50 | ] 51 | 52 | [[package]] 53 | name = "equivalent" 54 | version = "1.0.1" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 57 | 58 | [[package]] 59 | name = "hashbrown" 60 | version = "0.14.3" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 63 | 64 | [[package]] 65 | name = "indexmap" 66 | version = "2.2.1" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "433de089bd45971eecf4668ee0ee8f4cec17db4f8bd8f7bc3197a6ce37aa7d9b" 69 | dependencies = [ 70 | "equivalent", 71 | "hashbrown", 72 | ] 73 | 74 | [[package]] 75 | name = "internal-iterator" 76 | version = "0.2.3" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "969ee3fc68ec2e88eb21434ce4d9b7e1600d1ce92ff974560a6c4a304f5124b9" 79 | 80 | [[package]] 81 | name = "itertools" 82 | version = "0.10.5" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 85 | dependencies = [ 86 | "either", 87 | ] 88 | 89 | [[package]] 90 | name = "itoa" 91 | version = "1.0.10" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" 94 | 95 | [[package]] 96 | name = "lazy_static" 97 | version = "1.4.0" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 100 | 101 | [[package]] 102 | name = "longest-increasing-subsequence" 103 | version = "0.1.0" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "b3bd0dd2cd90571056fdb71f6275fada10131182f84899f4b2a916e565d81d86" 106 | 107 | [[package]] 108 | name = "proc-macro2" 109 | version = "1.0.78" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" 112 | dependencies = [ 113 | "unicode-ident", 114 | ] 115 | 116 | [[package]] 117 | name = "quote" 118 | version = "1.0.35" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 121 | dependencies = [ 122 | "proc-macro2", 123 | ] 124 | 125 | [[package]] 126 | name = "rustc-hash" 127 | version = "1.1.0" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 130 | 131 | [[package]] 132 | name = "rustc_version" 133 | version = "0.4.0" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 136 | dependencies = [ 137 | "semver", 138 | ] 139 | 140 | [[package]] 141 | name = "ryu" 142 | version = "1.0.16" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" 145 | 146 | [[package]] 147 | name = "semver" 148 | version = "1.0.21" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" 151 | 152 | [[package]] 153 | name = "serde" 154 | version = "1.0.196" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" 157 | dependencies = [ 158 | "serde_derive", 159 | ] 160 | 161 | [[package]] 162 | name = "serde_derive" 163 | version = "1.0.196" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" 166 | dependencies = [ 167 | "proc-macro2", 168 | "quote", 169 | "syn 2.0.48", 170 | ] 171 | 172 | [[package]] 173 | name = "serde_json" 174 | version = "1.0.113" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" 177 | dependencies = [ 178 | "itoa", 179 | "ryu", 180 | "serde", 181 | ] 182 | 183 | [[package]] 184 | name = "smallvec" 185 | version = "1.13.1" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" 188 | dependencies = [ 189 | "serde", 190 | ] 191 | 192 | [[package]] 193 | name = "spirt" 194 | version = "0.3.0" 195 | dependencies = [ 196 | "arrayvec", 197 | "bytemuck", 198 | "derive_more", 199 | "elsa", 200 | "indexmap", 201 | "internal-iterator", 202 | "itertools", 203 | "lazy_static", 204 | "longest-increasing-subsequence", 205 | "rustc-hash", 206 | "serde", 207 | "serde_json", 208 | "smallvec", 209 | ] 210 | 211 | [[package]] 212 | name = "stable_deref_trait" 213 | version = "1.2.0" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 216 | 217 | [[package]] 218 | name = "syn" 219 | version = "1.0.109" 220 | source = "registry+https://github.com/rust-lang/crates.io-index" 221 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 222 | dependencies = [ 223 | "proc-macro2", 224 | "quote", 225 | "unicode-ident", 226 | ] 227 | 228 | [[package]] 229 | name = "syn" 230 | version = "2.0.48" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" 233 | dependencies = [ 234 | "proc-macro2", 235 | "quote", 236 | "unicode-ident", 237 | ] 238 | 239 | [[package]] 240 | name = "unicode-ident" 241 | version = "1.0.12" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 244 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Changelog 4 | 5 | All notable changes to this project will be documented in this file. 6 | 7 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 8 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 9 | 10 | 31 | 32 | 33 | 34 | ## [Unreleased] - ReleaseDate 35 | 36 | ### Changed 🛠 37 | - [PR#61](https://github.com/EmbarkStudios/spirt/pull/61) updated `SPIRV-Headers` 38 | to match Vulkan SDK 1.3.275 39 | - [PR#55](https://github.com/EmbarkStudios/spirt/pull/55) fixed CFG structurization 40 | "region `children` list desync" assertion failures (e.g. [rust-gpu#1086](https://github.com/EmbarkStudios/rust-gpu/issues/1086)) 41 | by tracking whole `ControlRegion`s instead of their `children` 42 | - also removed a lot of redundant boolean values, thanks to condition propagation 43 | becoming always on-demand (instead of relying on less robust special-casing) 44 | - [PR#51](https://github.com/EmbarkStudios/spirt/pull/51) combined `TypeCtor`/`ConstCtor` 45 | and their respective "ctor args", into a single unified `TypeKind`/`ConstKind` 46 | - [PR#48](https://github.com/EmbarkStudios/spirt/pull/48) changed CFG structurization 47 | from "maximal loops" to "minimal loops" (computed using Tarjan's SCC algorithm), 48 | and added `OpLoopMerge` support on top (by extending a "minimal loop" as needed) 49 | 50 | ## [0.3.0] - 2023-07-25 51 | 52 | ### Added ⭐ 53 | - [PR#45](https://github.com/EmbarkStudios/spirt/pull/45) added the ability to 54 | pretty-print `OpExtInst`s (SPIR-V "extended instructions") using official 55 | `extinst.*.grammar.json` descriptions and/or custom ones (registered via `Context`) 56 | 57 | ### Changed 🛠 58 | - [PR#43](https://github.com/EmbarkStudios/spirt/pull/43) tweaked several pretty-printing 59 | details to improve visual cohesion ("named arguments" in `module.{dialect,debuginfo}`) 60 | and ergonomics (multi-line string literals, HTML entities for anchor escaping, 61 | hover on multi-version table cells to disable "no changes" desaturation/dimming) 62 | - [PR#36](https://github.com/EmbarkStudios/spirt/pull/36) started using `OpName`s 63 | in pretty-printing, to replace the `T1`/`F2`/`v3` "anonymous" style, when unambiguous 64 | - [PR#40](https://github.com/EmbarkStudios/spirt/pull/40) increased the pretty-printed 65 | HTML `font-size` from `15px` to `17px`, to improve readability 66 | - [PR#39](https://github.com/EmbarkStudios/spirt/pull/39) shortened pretty-printed names 67 | like `type2`/`func3`/etc. to `T2`/`F3`/etc. (with e.g. `type T2 = ...` style definitions) 68 | - [PR#38](https://github.com/EmbarkStudios/spirt/pull/38) split off `print::Node::Root`, 69 | allowing "roots" and "non-root nodes" to have different APIs, and dynamic dispatch 70 | to be limited to "roots" (as "non-root nodes" are a small finite set of types) 71 | - [PR#35](https://github.com/EmbarkStudios/spirt/pull/35) abandoned the custom 72 | `#{A, B, C}` "attribute set" style in favor of Rust-like `#[A]` `#[B]` `#[C]` 73 | (and always printing them inline, without any `attrs123` shorthands) 74 | - [PR#33](https://github.com/EmbarkStudios/spirt/pull/33) replaced the `spv.OpFoo(IDs)` 75 | style of pretty-printing with `spv.OpFoo(A: imm, B: ID, C: imm, ...)` (unified parenthesized 76 | list of operands, with deemphasized operand names in `foo:` "named arguments" style) 77 | - [PR#28](https://github.com/EmbarkStudios/spirt/pull/28) moved two `DataInstDef` 78 | fields (`kind` and `output_type`) to `DataInstForm`, a new interned type 79 | - [PR#30](https://github.com/EmbarkStudios/spirt/pull/30) replaced the old `spv-lower-dump` 80 | example (which only dumped plaintext, not HTML) with a more useful `spv-lower-print` one 81 | 82 | ### Fixed 🩹 83 | - [PR#34](https://github.com/EmbarkStudios/spirt/pull/34) fixed `OpTypePointer`s being 84 | spuriously printed as dependencies of `GlobalVarDecl`/`PtrToGlobalVar` (neither of 85 | which actually print the pointer type), and added a CI check for `README.md` examples 86 | - [PR#37](https://github.com/EmbarkStudios/spirt/pull/37) fixed pretty-printing layout 87 | accuracy regarding line widths (by tracking `font-size`-aware "fractional columns"), 88 | and raised the maximum line width back up to `120` columns 89 | - [PR#27](https://github.com/EmbarkStudios/spirt/pull/27) fixed some pretty-printing issues 90 | in the initial `Attr::Diagnostics` implementation (`BUG` paths and `/* ... */` indentation) 91 | 92 | ## [0.2.0] - 2023-04-21 93 | 94 | ### Added ⭐ 95 | - [PR#24](https://github.com/EmbarkStudios/spirt/pull/24) added `qptr` ("quasi-pointer") type 96 | and associated passes to destroy and recreate pointer-related type information 97 | (see [PR#24](https://github.com/EmbarkStudios/spirt/pull/24) for a much more detailed overview) 98 | - [PR#22](https://github.com/EmbarkStudios/spirt/pull/22) added `Diag` and `Attr::Diagnostics`, 99 | for embedding diagnostics (errors or warnings) in SPIR-T itself 100 | - [PR#18](https://github.com/EmbarkStudios/spirt/pull/18) added anchor-based alignment 101 | to multi-version pretty-printing output (the same definitions will be kept on 102 | the same lines in all columns, wherever possible, to improve readability) 103 | 104 | ### Changed 🛠 105 | - [PR#26](https://github.com/EmbarkStudios/spirt/pull/26) allowed using `OpEmitMeshTasksEXT` as a terminator (by hardcoding it as `Control-Flow`) 106 | - [PR#25](https://github.com/EmbarkStudios/spirt/pull/25) updated SPIRV-headers from 1.5.4 to 1.6.1 107 | - [PR#21](https://github.com/EmbarkStudios/spirt/pull/21) tweaked pretty-printing 108 | styles around de-emphasis (shrinking instead of thinning, for width savings), 109 | and SPIR-V ops/enums (via de-emphasized `spv.` prefix and distinct orange color) 110 | 111 | ## [0.1.0] - 2022-12-16 112 | *Initial public release of SPIR-T for minimal Rust-GPU integration.* 113 | ### Added ⭐ 114 | - Conversions from/to SPIR-V (`spv::lower`/`spv::lift`). 115 | - Control-flow structurizer, from CFGs to SPIR-T's stricter structured control-flow. 116 | - Pretty-printer with (styled and hyperlinked) HTML output. 117 | 118 | 119 | [Unreleased]: https://github.com/EmbarkStudios/spirt/compare/0.3.0...HEAD 120 | [0.3.0]: https://github.com/EmbarkStudios/spirt/compare/0.2.0...0.3.0 121 | [0.2.0]: https://github.com/EmbarkStudios/spirt/compare/0.1.0...0.2.0 122 | 129 | [0.1.0]: https://github.com/EmbarkStudios/spirt/compare/c5d63c6974d03e1495eba2c72ff403a246586a40...0.1.0 130 | -------------------------------------------------------------------------------- /src/func_at.rs: -------------------------------------------------------------------------------- 1 | //! Traversal helpers for intra-function entities. 2 | //! 3 | //! [`FuncAt

`]/[`FuncAtMut

`] are like `(&FuncDefBody, P)`/`(&mut FuncDefBody, P`) 4 | //! (where `P` is some type describing a "position" in the function), except: 5 | //! * they only borrow the [`EntityDefs`] fields of [`FuncDefBody`] 6 | //! * this can prevent borrow conflicts, especially when mutating other fields 7 | //! * it also avoids accidentally accessing parts of the function definition 8 | //! without going through `P` (as [`EntityDefs`] requires keys for any access) 9 | //! * they're dedicated types with inherent methods and trait `impl`s 10 | 11 | // NOTE(eddyb) wrong wrt lifetimes (https://github.com/rust-lang/rust-clippy/issues/5004). 12 | #![allow(clippy::should_implement_trait)] 13 | 14 | use crate::{ 15 | Context, ControlNode, ControlNodeDef, ControlRegion, ControlRegionDef, DataInst, DataInstDef, 16 | EntityDefs, EntityList, EntityListIter, FuncDefBody, Type, Value, 17 | }; 18 | 19 | /// Immutable traversal (i.e. visiting) helper for intra-function entities. 20 | /// 21 | /// The point/position type `P` should be an entity or a shallow entity wrapper 22 | /// (e.g. [`EntityList`]). 23 | #[derive(Copy, Clone)] 24 | pub struct FuncAt<'a, P: Copy> { 25 | pub control_regions: &'a EntityDefs, 26 | pub control_nodes: &'a EntityDefs, 27 | pub data_insts: &'a EntityDefs, 28 | 29 | pub position: P, 30 | } 31 | 32 | impl<'a, P: Copy> FuncAt<'a, P> { 33 | /// Reposition to `new_position`. 34 | pub fn at(self, new_position: P2) -> FuncAt<'a, P2> { 35 | FuncAt { 36 | control_regions: self.control_regions, 37 | control_nodes: self.control_nodes, 38 | data_insts: self.data_insts, 39 | position: new_position, 40 | } 41 | } 42 | } 43 | 44 | impl<'a> FuncAt<'a, ControlRegion> { 45 | pub fn def(self) -> &'a ControlRegionDef { 46 | &self.control_regions[self.position] 47 | } 48 | 49 | pub fn at_children(self) -> FuncAt<'a, EntityList> { 50 | self.at(self.def().children) 51 | } 52 | } 53 | 54 | impl<'a> IntoIterator for FuncAt<'a, EntityList> { 55 | type IntoIter = FuncAt<'a, EntityListIter>; 56 | type Item = FuncAt<'a, ControlNode>; 57 | fn into_iter(self) -> Self::IntoIter { 58 | self.at(self.position.iter()) 59 | } 60 | } 61 | 62 | impl<'a> Iterator for FuncAt<'a, EntityListIter> { 63 | type Item = FuncAt<'a, ControlNode>; 64 | fn next(&mut self) -> Option { 65 | let (next, rest) = self.position.split_first(self.control_nodes)?; 66 | self.position = rest; 67 | Some(self.at(next)) 68 | } 69 | } 70 | 71 | impl<'a> FuncAt<'a, ControlNode> { 72 | pub fn def(self) -> &'a ControlNodeDef { 73 | &self.control_nodes[self.position] 74 | } 75 | } 76 | 77 | impl<'a> IntoIterator for FuncAt<'a, EntityList> { 78 | type IntoIter = FuncAt<'a, EntityListIter>; 79 | type Item = FuncAt<'a, DataInst>; 80 | fn into_iter(self) -> Self::IntoIter { 81 | self.at(self.position.iter()) 82 | } 83 | } 84 | 85 | impl<'a> Iterator for FuncAt<'a, EntityListIter> { 86 | type Item = FuncAt<'a, DataInst>; 87 | fn next(&mut self) -> Option { 88 | let (next, rest) = self.position.split_first(self.data_insts)?; 89 | self.position = rest; 90 | Some(self.at(next)) 91 | } 92 | } 93 | 94 | impl<'a> DoubleEndedIterator for FuncAt<'a, EntityListIter> { 95 | fn next_back(&mut self) -> Option { 96 | let (prev, rest) = self.position.split_last(self.data_insts)?; 97 | self.position = rest; 98 | Some(self.at(prev)) 99 | } 100 | } 101 | 102 | impl<'a> FuncAt<'a, DataInst> { 103 | pub fn def(self) -> &'a DataInstDef { 104 | &self.data_insts[self.position] 105 | } 106 | } 107 | 108 | impl FuncAt<'_, Value> { 109 | /// Return the [`Type`] of this [`Value`] ([`Context`] used for [`Value::Const`]). 110 | pub fn type_of(self, cx: &Context) -> Type { 111 | match self.position { 112 | Value::Const(ct) => cx[ct].ty, 113 | Value::ControlRegionInput { region, input_idx } => { 114 | self.at(region).def().inputs[input_idx as usize].ty 115 | } 116 | Value::ControlNodeOutput { control_node, output_idx } => { 117 | self.at(control_node).def().outputs[output_idx as usize].ty 118 | } 119 | Value::DataInstOutput(inst) => cx[self.at(inst).def().form].output_type.unwrap(), 120 | } 121 | } 122 | } 123 | 124 | /// Mutable traversal (i.e. transforming) helper for intra-function entities. 125 | /// 126 | /// The point/position type `P` should be an entity or a shallow entity wrapper 127 | /// (e.g. [`EntityList`]). 128 | pub struct FuncAtMut<'a, P: Copy> { 129 | pub control_regions: &'a mut EntityDefs, 130 | pub control_nodes: &'a mut EntityDefs, 131 | pub data_insts: &'a mut EntityDefs, 132 | 133 | pub position: P, 134 | } 135 | 136 | impl<'a, P: Copy> FuncAtMut<'a, P> { 137 | /// Emulate a "reborrow", which is automatic only for `&mut` types. 138 | pub fn reborrow(&mut self) -> FuncAtMut<'_, P> { 139 | FuncAtMut { 140 | control_regions: self.control_regions, 141 | control_nodes: self.control_nodes, 142 | data_insts: self.data_insts, 143 | position: self.position, 144 | } 145 | } 146 | 147 | /// Reposition to `new_position`. 148 | pub fn at(self, new_position: P2) -> FuncAtMut<'a, P2> { 149 | FuncAtMut { 150 | control_regions: self.control_regions, 151 | control_nodes: self.control_nodes, 152 | data_insts: self.data_insts, 153 | position: new_position, 154 | } 155 | } 156 | 157 | /// Demote to a `FuncAt`, with the same `position`. 158 | // 159 | // FIXME(eddyb) maybe find a better name for this? 160 | pub fn freeze(self) -> FuncAt<'a, P> { 161 | let FuncAtMut { control_regions, control_nodes, data_insts, position } = self; 162 | FuncAt { control_regions, control_nodes, data_insts, position } 163 | } 164 | } 165 | 166 | impl<'a> FuncAtMut<'a, ControlRegion> { 167 | pub fn def(self) -> &'a mut ControlRegionDef { 168 | &mut self.control_regions[self.position] 169 | } 170 | 171 | pub fn at_children(mut self) -> FuncAtMut<'a, EntityList> { 172 | let children = self.reborrow().def().children; 173 | self.at(children) 174 | } 175 | } 176 | 177 | // HACK(eddyb) can't implement `IntoIterator` because `next` borrows `self`. 178 | impl<'a> FuncAtMut<'a, EntityList> { 179 | pub fn into_iter(self) -> FuncAtMut<'a, EntityListIter> { 180 | let iter = self.position.iter(); 181 | self.at(iter) 182 | } 183 | } 184 | 185 | // HACK(eddyb) can't implement `Iterator` because `next` borrows `self`. 186 | impl FuncAtMut<'_, EntityListIter> { 187 | pub fn next(&mut self) -> Option> { 188 | let (next, rest) = self.position.split_first(self.control_nodes)?; 189 | self.position = rest; 190 | Some(self.reborrow().at(next)) 191 | } 192 | } 193 | 194 | impl<'a> FuncAtMut<'a, ControlNode> { 195 | pub fn def(self) -> &'a mut ControlNodeDef { 196 | &mut self.control_nodes[self.position] 197 | } 198 | } 199 | 200 | // HACK(eddyb) can't implement `IntoIterator` because `next` borrows `self`. 201 | impl<'a> FuncAtMut<'a, EntityList> { 202 | pub fn into_iter(self) -> FuncAtMut<'a, EntityListIter> { 203 | let iter = self.position.iter(); 204 | self.at(iter) 205 | } 206 | } 207 | 208 | // HACK(eddyb) can't implement `Iterator` because `next` borrows `self`. 209 | impl FuncAtMut<'_, EntityListIter> { 210 | pub fn next(&mut self) -> Option> { 211 | let (next, rest) = self.position.split_first(self.data_insts)?; 212 | self.position = rest; 213 | Some(self.reborrow().at(next)) 214 | } 215 | } 216 | 217 | impl<'a> FuncAtMut<'a, DataInst> { 218 | pub fn def(self) -> &'a mut DataInstDef { 219 | &mut self.data_insts[self.position] 220 | } 221 | } 222 | 223 | impl FuncDefBody { 224 | /// Start immutably traversing the function at `position`. 225 | pub fn at(&self, position: P) -> FuncAt<'_, P> { 226 | FuncAt { 227 | control_regions: &self.control_regions, 228 | control_nodes: &self.control_nodes, 229 | data_insts: &self.data_insts, 230 | position, 231 | } 232 | } 233 | 234 | /// Start mutably traversing the function at `position`. 235 | pub fn at_mut(&mut self, position: P) -> FuncAtMut<'_, P> { 236 | FuncAtMut { 237 | control_regions: &mut self.control_regions, 238 | control_nodes: &mut self.control_nodes, 239 | data_insts: &mut self.data_insts, 240 | position, 241 | } 242 | } 243 | 244 | /// Shorthand for `func_def_body.at(func_def_body.body)`. 245 | pub fn at_body(&self) -> FuncAt<'_, ControlRegion> { 246 | self.at(self.body) 247 | } 248 | 249 | /// Shorthand for `func_def_body.at_mut(func_def_body.body)`. 250 | pub fn at_mut_body(&mut self) -> FuncAtMut<'_, ControlRegion> { 251 | self.at_mut(self.body) 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/spv/write.rs: -------------------------------------------------------------------------------- 1 | //! Low-level emission of SPIR-V binary form. 2 | 3 | use crate::spv::{self, spec}; 4 | use std::borrow::Cow; 5 | use std::path::Path; 6 | use std::{fs, io, iter, slice}; 7 | 8 | // FIXME(eddyb) keep a `&'static spec::Spec` if that can even speed up anything. 9 | struct OperandEmitter<'a> { 10 | /// Input immediate operands of an instruction. 11 | imms: iter::Copied>, 12 | 13 | /// Input ID operands of an instruction. 14 | ids: iter::Copied>, 15 | 16 | /// Output SPIR-V words. 17 | out: &'a mut Vec, 18 | } 19 | 20 | enum OperandEmitError { 21 | /// Ran out of immediates while emitting an instruction's operands. 22 | NotEnoughImms, 23 | 24 | /// Ran out of IDs while emitting an instruction's operands. 25 | NotEnoughIds, 26 | 27 | /// Extra immediates were left over, after emitting an instruction's operands. 28 | TooManyImms, 29 | 30 | /// Extra IDs were left over, after emitting an instruction's operands. 31 | TooManyIds, 32 | 33 | /// Unsupported enumerand value. 34 | UnsupportedEnumerand(spec::OperandKind, u32), 35 | } 36 | 37 | impl OperandEmitError { 38 | // FIXME(eddyb) improve messages and add more contextual information. 39 | fn message(&self) -> Cow<'static, str> { 40 | match *self { 41 | Self::NotEnoughImms => "truncated instruction (immediates)".into(), 42 | Self::NotEnoughIds => "truncated instruction (IDs)".into(), 43 | Self::TooManyImms => "overlong instruction (immediates)".into(), 44 | Self::TooManyIds => "overlong instruction (IDs)".into(), 45 | // FIXME(eddyb) deduplicate this with `spv::read`. 46 | Self::UnsupportedEnumerand(kind, word) => { 47 | let (name, def) = kind.name_and_def(); 48 | match def { 49 | spec::OperandKindDef::BitEnum { bits, .. } => { 50 | let unsupported = spec::BitIdx::of_all_set_bits(word) 51 | .filter(|&bit_idx| bits.get(bit_idx).is_none()) 52 | .fold(0u32, |x, i| x | (1 << i.0)); 53 | format!("unsupported {name} bit-pattern 0x{unsupported:08x}").into() 54 | } 55 | 56 | spec::OperandKindDef::ValueEnum { .. } => { 57 | format!("unsupported {name} value {word}").into() 58 | } 59 | 60 | _ => unreachable!(), 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | impl OperandEmitter<'_> { 68 | fn is_exhausted(&mut self) -> bool { 69 | // FIXME(eddyb) use `self.imms.is_empty() && self.ids.is_empty()` when 70 | // that is stabilized. 71 | self.imms.len() == 0 && self.ids.len() == 0 72 | } 73 | 74 | fn enumerant_params(&mut self, enumerant: &spec::Enumerant) -> Result<(), OperandEmitError> { 75 | for (mode, kind) in enumerant.all_params() { 76 | if mode == spec::OperandMode::Optional && self.is_exhausted() { 77 | break; 78 | } 79 | self.operand(kind)?; 80 | } 81 | 82 | Ok(()) 83 | } 84 | 85 | fn operand(&mut self, kind: spec::OperandKind) -> Result<(), OperandEmitError> { 86 | use OperandEmitError as Error; 87 | 88 | let mut get_enum_word = || match self.imms.next() { 89 | Some(spv::Imm::Short(found_kind, word)) => { 90 | assert_eq!(kind, found_kind); 91 | Ok(word) 92 | } 93 | Some(spv::Imm::LongStart(..) | spv::Imm::LongCont(..)) => unreachable!(), 94 | None => Err(Error::NotEnoughImms), 95 | }; 96 | 97 | match kind.def() { 98 | spec::OperandKindDef::BitEnum { bits, .. } => { 99 | let word = get_enum_word()?; 100 | self.out.push(word); 101 | 102 | for bit_idx in spec::BitIdx::of_all_set_bits(word) { 103 | let bit_def = 104 | bits.get(bit_idx).ok_or(Error::UnsupportedEnumerand(kind, word))?; 105 | self.enumerant_params(bit_def)?; 106 | } 107 | } 108 | spec::OperandKindDef::ValueEnum { variants } => { 109 | let word = get_enum_word()?; 110 | self.out.push(word); 111 | 112 | let variant_def = u16::try_from(word) 113 | .ok() 114 | .and_then(|v| variants.get(v)) 115 | .ok_or(Error::UnsupportedEnumerand(kind, word))?; 116 | self.enumerant_params(variant_def)?; 117 | } 118 | spec::OperandKindDef::Id => { 119 | self.out.push(self.ids.next().ok_or(Error::NotEnoughIds)?.get()); 120 | } 121 | spec::OperandKindDef::Literal { .. } => { 122 | match self.imms.next().ok_or(Error::NotEnoughImms)? { 123 | spv::Imm::Short(found_kind, word) => { 124 | assert_eq!(kind, found_kind); 125 | self.out.push(word); 126 | } 127 | spv::Imm::LongStart(found_kind, word) => { 128 | assert_eq!(kind, found_kind); 129 | self.out.push(word); 130 | while let Some(spv::Imm::LongCont(cont_kind, word)) = 131 | self.imms.clone().next() 132 | { 133 | self.imms.next(); 134 | assert_eq!(kind, cont_kind); 135 | self.out.push(word); 136 | } 137 | } 138 | spv::Imm::LongCont(..) => unreachable!(), 139 | } 140 | } 141 | } 142 | 143 | Ok(()) 144 | } 145 | 146 | fn inst_operands(mut self, def: &spec::InstructionDef) -> Result<(), OperandEmitError> { 147 | use OperandEmitError as Error; 148 | 149 | for (mode, kind) in def.all_operands() { 150 | if mode == spec::OperandMode::Optional && self.is_exhausted() { 151 | break; 152 | } 153 | self.operand(kind)?; 154 | } 155 | 156 | // The instruction must consume all of its operands. 157 | if !self.is_exhausted() { 158 | return Err( 159 | // FIXME(eddyb) use `!self.imms.is_empty()` when that is stabilized. 160 | if self.imms.len() != 0 { 161 | Error::TooManyImms 162 | } else { 163 | // FIXME(eddyb) use `!self.ids.is_empty()` when that is stabilized. 164 | assert!(self.ids.len() != 0); 165 | Error::TooManyIds 166 | }, 167 | ); 168 | } 169 | 170 | Ok(()) 171 | } 172 | } 173 | 174 | pub struct ModuleEmitter { 175 | /// Output SPIR-V words. 176 | // FIXME(eddyb) try to write bytes to an `impl io::Write` directly. 177 | pub words: Vec, 178 | } 179 | 180 | // FIXME(eddyb) stop abusing `io::Error` for error reporting. 181 | fn invalid(reason: &str) -> io::Error { 182 | io::Error::new(io::ErrorKind::InvalidData, format!("malformed SPIR-V ({reason})")) 183 | } 184 | 185 | impl ModuleEmitter { 186 | pub fn with_header(header: [u32; spec::HEADER_LEN]) -> Self { 187 | // FIXME(eddyb) sanity-check the provided header words. 188 | Self { words: header.into() } 189 | } 190 | 191 | // FIXME(eddyb) sanity-check the operands against the definition of `inst.opcode`. 192 | pub fn push_inst(&mut self, inst: &spv::InstWithIds) -> io::Result<()> { 193 | let (inst_name, def) = inst.opcode.name_and_def(); 194 | let invalid = |msg: &str| invalid(&format!("in {inst_name}: {msg}")); 195 | 196 | // FIXME(eddyb) make these errors clearer (or turn them into asserts?). 197 | if inst.result_type_id.is_some() != def.has_result_type_id { 198 | return Err(invalid("result type ID (`IdResultType`) mismatch")); 199 | } 200 | if inst.result_id.is_some() != def.has_result_id { 201 | return Err(invalid("result ID (`IdResult`) mismatch")); 202 | } 203 | 204 | let total_word_count = 1 205 | + (inst.result_type_id.is_some() as usize) 206 | + (inst.result_id.is_some() as usize) 207 | + inst.imms.len() 208 | + inst.ids.len(); 209 | 210 | self.words.reserve(total_word_count); 211 | let expected_final_pos = self.words.len() + total_word_count; 212 | 213 | let opcode = u32::from(inst.opcode.as_u16()) 214 | | u32::from(u16::try_from(total_word_count).ok().ok_or_else(|| { 215 | invalid("word count of SPIR-V instruction doesn't fit in 16 bits") 216 | })?) << 16; 217 | self.words.extend( 218 | iter::once(opcode) 219 | .chain(inst.result_type_id.map(|id| id.get())) 220 | .chain(inst.result_id.map(|id| id.get())), 221 | ); 222 | 223 | OperandEmitter { 224 | imms: inst.imms.iter().copied(), 225 | ids: inst.ids.iter().copied(), 226 | out: &mut self.words, 227 | } 228 | .inst_operands(def) 229 | .map_err(|e| invalid(&e.message()))?; 230 | 231 | // If no error was produced so far, `OperandEmitter` should've pushed 232 | // the exact number of words. 233 | assert_eq!(self.words.len(), expected_final_pos); 234 | 235 | Ok(()) 236 | } 237 | 238 | pub fn write_to_spv_file(&self, path: impl AsRef) -> io::Result<()> { 239 | fs::write(path, bytemuck::cast_slice::(&self.words)) 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/qptr/mod.rs: -------------------------------------------------------------------------------- 1 | //! [`QPtr`](crate::TypeKind::QPtr)-related type definitions and passes. 2 | // 3 | // FIXME(eddyb) consider `#[cfg(doc)] use crate::TypeKind::QPtr;` for doc comments. 4 | // FIXME(eddyb) PR description of https://github.com/EmbarkStudios/spirt/pull/24 5 | // has more useful docs that could be copied here. 6 | 7 | use crate::{AddrSpace, OrdAssertEq, Type}; 8 | use std::collections::BTreeMap; 9 | use std::num::NonZeroU32; 10 | use std::ops::Range; 11 | use std::rc::Rc; 12 | 13 | // NOTE(eddyb) all the modules are declared here, but they're documented "inside" 14 | // (i.e. using inner doc comments). 15 | pub mod analyze; 16 | mod layout; 17 | pub mod lift; 18 | pub mod lower; 19 | pub mod shapes; 20 | 21 | pub use layout::LayoutConfig; 22 | 23 | /// `QPtr`-specific attributes ([`Attr::QPtr`]). 24 | #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 25 | pub enum QPtrAttr { 26 | /// When applied to a `DataInst` with a `QPtr`-typed `inputs[input_idx]`, 27 | /// this describes the original `OpTypePointer` consumed by an unknown 28 | /// SPIR-V instruction (which may, or may not, access memory, at all). 29 | /// 30 | /// Assumes the original SPIR-V `StorageClass` is redundant (i.e. can be 31 | /// deduced from the pointer's provenance), and that any accesses performed 32 | /// through the pointer (or any pointers derived from it) stay within bounds 33 | /// (i.e. logical pointer semantics, unsuited for e.g. `OpPtrAccessChain`). 34 | // 35 | // FIXME(eddyb) reduce usage by modeling more of SPIR-V inside SPIR-T. 36 | ToSpvPtrInput { input_idx: u32, pointee: OrdAssertEq }, 37 | 38 | /// When applied to a `DataInst` with a `QPtr`-typed output value, 39 | /// this describes the original `OpTypePointer` produced by an unknown 40 | /// SPIR-V instruction (likely creating it, without deriving from an input). 41 | /// 42 | /// Assumes the original SPIR-V `StorageClass` is significant (e.g. fresh 43 | /// provenance being created on the fly via `OpConvertUToPtr`, or derived 44 | /// internally by the implementation via `OpImageTexelPointer`). 45 | // 46 | // FIXME(eddyb) reduce usage by modeling more of SPIR-V inside SPIR-T, or 47 | // at least using some kind of bitcast instead of `QPtr` + this attribute. 48 | // FIXME(eddyb) `OpConvertUToPtr` creates a physical pointer, could we avoid 49 | // dealing with those at all in `QPtr`? (as its focus is logical legalization) 50 | FromSpvPtrOutput { 51 | // FIXME(eddyb) should this use a special `spv::StorageClass` type? 52 | addr_space: OrdAssertEq, 53 | pointee: OrdAssertEq, 54 | }, 55 | 56 | /// When applied to a `QPtr`-typed `GlobalVar`, `DataInst`, 57 | /// `ControlRegionInputDecl` or `ControlNodeOutputDecl`, this tracks all the 58 | /// ways in which the pointer may be used (see `QPtrUsage`). 59 | Usage(OrdAssertEq), 60 | } 61 | 62 | #[derive(Clone, PartialEq, Eq, Hash)] 63 | pub enum QPtrUsage { 64 | /// Used to access one or more handles (i.e. optionally indexed by 65 | /// [`QPtrOp::HandleArrayIndex`]), which can be: 66 | /// - `Handle::Opaque(handle_type)`: all uses involve [`QPtrOp::Load`] or 67 | /// [`QPtrAttr::ToSpvPtrInput`], with the common type `handle_type` 68 | /// - `Handle::Buffer(data_usage)`: carries with it `data_usage`, i.e. the 69 | /// usage of the memory that can be accessed through [`QPtrOp::BufferData`] 70 | Handles(shapes::Handle), 71 | 72 | // FIXME(eddyb) unify terminology around "concrete"/"memory"/"untyped (data)". 73 | Memory(QPtrMemUsage), 74 | } 75 | 76 | #[derive(Clone, PartialEq, Eq, Hash)] 77 | pub struct QPtrMemUsage { 78 | /// If present, this is a worst-case upper bound on memory accesses that may 79 | /// be performed through this pointer. 80 | // 81 | // FIXME(eddyb) use proper newtypes for byte amounts. 82 | // 83 | // FIXME(eddyb) suboptimal naming choice, but other options are too verbose, 84 | // including maybe using `RangeTo<_>` to explicitly indicate "exclusive". 85 | // 86 | // FIXME(eddyb) consider renaming such information to "extent", but that might 87 | // be ambiguous with an offset range (as opposed to min/max of *possible* 88 | // `offset_range.end`, i.e. "size"). 89 | pub max_size: Option, 90 | 91 | pub kind: QPtrMemUsageKind, 92 | } 93 | 94 | impl QPtrMemUsage { 95 | pub const UNUSED: Self = Self { max_size: Some(0), kind: QPtrMemUsageKind::Unused }; 96 | } 97 | 98 | #[derive(Clone, PartialEq, Eq, Hash)] 99 | pub enum QPtrMemUsageKind { 100 | /// Not actually used, which could be caused by pointer offsetting operations 101 | /// with unused results, or as an intermediary state during analyses. 102 | Unused, 103 | 104 | // FIXME(eddyb) replace the two leaves with e.g. `Leaf(Type, QPtrMemLeafUsage)`. 105 | // 106 | // 107 | // 108 | /// Used as a typed pointer (e.g. via unknown SPIR-V instructions), requiring 109 | /// a specific choice of pointee type which cannot be modified, and has to be 110 | /// reused as-is when lifting `QPtr`s back to typed pointers. 111 | /// 112 | /// Other overlapping uses can be merged into this one as long as they can 113 | /// be fully expressed using the (transitive) components of this type. 114 | StrictlyTyped(Type), 115 | 116 | /// Used directly to access memory (e.g. [`QPtrOp::Load`], [`QPtrOp::Store`]), 117 | /// which can be decomposed as necessary (down to individual scalar leaves), 118 | /// to allow maximal merging opportunities. 119 | // 120 | // FIXME(eddyb) track whether `Load`s and/or `Store`s are used, so that we 121 | // can infer `NonWritable`/`NonReadable` annotations as well. 122 | DirectAccess(Type), 123 | 124 | /// Used as a common base for (constant) offsetting, which requires it to have 125 | /// its own (aggregate) type, when lifting `QPtr`s back to typed pointers. 126 | OffsetBase(Rc>), 127 | 128 | /// Used as a common base for (dynamic) offsetting, which requires it to have 129 | /// its own (array) type, when lifting `QPtr`s back to typed pointers, with 130 | /// one single element type being repeated across the entire size. 131 | DynOffsetBase { 132 | // FIXME(eddyb) this feels inefficient. 133 | element: Rc, 134 | stride: NonZeroU32, 135 | }, 136 | // FIXME(eddyb) consider adding an `Union` case for driving legalization. 137 | } 138 | 139 | /// `QPtr`-specific operations ([`DataInstKind::QPtr`]). 140 | #[derive(Clone, PartialEq, Eq, Hash)] 141 | pub enum QPtrOp { 142 | // HACK(eddyb) `OpVariable` replacement, which itself should not be kept as 143 | // a `SpvInst` - once fn-local variables are lowered, this should go there. 144 | FuncLocalVar(shapes::MemLayout), 145 | 146 | /// Adjust a **handle array** `QPtr` (`inputs[0]`), by selecting the handle 147 | /// at the index (`inputs[1]`) from the handle array (i.e. the resulting 148 | /// `QPtr` is limited to that one handle and can't be further "moved around"). 149 | // 150 | // FIXME(eddyb) this could maybe use `DynOffset`, if `stride` is changed to 151 | // be `enum { Handle, Bytes(u32) }`, but that feels a bit too much? 152 | HandleArrayIndex, 153 | 154 | /// Get a **memory** `QPtr` pointing at the contents of the buffer whose 155 | /// handle is (implicitly) loaded from a **handle** `QPtr` (`inputs[0]`). 156 | // 157 | // FIXME(eddyb) should buffers be a `Type` of their own, that can be loaded 158 | // from a handle `QPtr`, and then has data pointer / length ops *on that*? 159 | BufferData, 160 | 161 | /// Get the length of the buffer whose handle is (implicitly) loaded from a 162 | /// **handle** `QPtr` (`inputs[0]`), converted to a count of "dynamic units" 163 | /// (as per [`shapes::MaybeDynMemLayout`]) by subtracting `fixed_base_size`, 164 | /// then dividing by `dyn_unit_stride`. 165 | // 166 | // FIXME(eddyb) should this handle _only_ "length in bytes", with additional 167 | // integer subtraction+division operations on lowering to `QPtr`, and then 168 | // multiplication+addition on lifting back to SPIR-V, followed by simplifying 169 | // the redundant `(x * a + b - b) / a` to just `x`? 170 | // 171 | // FIXME(eddyb) actually lower `OpArrayLength` to this! 172 | BufferDynLen { 173 | fixed_base_size: u32, 174 | dyn_unit_stride: NonZeroU32, 175 | }, 176 | 177 | /// Adjust a **memory** `QPtr` (`inputs[0]`), by adding a (signed) immediate 178 | /// amount of bytes to its "address" (whether physical or conceptual). 179 | // 180 | // FIXME(eddyb) some kind of `inbounds` would be very useful here, up to and 181 | // including "capability slicing" to limit the usable range of the output. 182 | Offset(i32), 183 | 184 | /// Adjust a **memory** `QPtr` (`inputs[0]`), by adding a (signed) dynamic 185 | /// "index" (`inputs[1]`), multiplied by `stride` (bytes per element), 186 | /// to its "address" (whether physical or conceptual). 187 | DynOffset { 188 | stride: NonZeroU32, 189 | 190 | /// Bounds on the dynamic "index" (`inputs[1]`). 191 | // 192 | // FIXME(eddyb) should this be an attribute/refinement? 193 | index_bounds: Option>, 194 | }, 195 | 196 | /// Read a single value from a `QPtr` (`inputs[0]`). 197 | // 198 | // FIXME(eddyb) limit this to memory, and scalars, maybe vectors at most. 199 | Load, 200 | 201 | /// Write a single value (`inputs[1]`) to a `QPtr` (`inputs[0]`). 202 | // 203 | // FIXME(eddyb) limit this to memory, and scalars, maybe vectors at most. 204 | Store, 205 | // 206 | // FIXME(eddyb) implement more ops! at the very least copying! 207 | // (and lowering could ignore pointercasts, I guess?) 208 | } 209 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

8 | 9 | # `SPIR-🇹` 10 | 11 | **⋯🢒 🇹arget 🠆 🇹ransform 🠆 🇹ranslate ⋯🢒** 12 | 13 | [![Embark](https://img.shields.io/badge/embark-open%20source-blueviolet.svg)](https://embark.dev) 14 | [![Crates.io](https://img.shields.io/crates/v/spirt.svg)](https://crates.io/crates/spirt) 15 | [![Docs](https://docs.rs/spirt/badge.svg)](https://docs.rs/spirt) 16 | [![Git Docs](https://img.shields.io/badge/git%20main%20docs-published-blue)](https://embarkstudios.github.io/spirt/spirt/index.html) 17 | [![dependency status](https://deps.rs/repo/github/EmbarkStudios/spirt/status.svg)](https://deps.rs/repo/github/EmbarkStudios/spirt) 18 | [![Build status](https://github.com/EmbarkStudios/spirt/workflows/CI/badge.svg)](https://github.com/EmbarkStudios/spirt/actions) 19 |
20 | 21 | **SPIR-🇹** is a research project aimed at exploring shader-oriented IR designs derived from SPIR-V, and producing a framework around such an IR to facilitate advanced compilation pipelines, beyond what existing SPIR-V tooling allows for. 22 | 23 | Such a need arose in the [Rust-GPU] project, which requires a variety of legalization passes to turn general-purpose (Rust1) code operating on *untyped* memory, into GPU-friendly direct data-flow. 24 | Our goal is to replace the existing [Rust-GPU] SPIR-V legalizations passes with **SPIR-🇹** equivalents - but even more imporantly, **SPIR-🇹** should allow writing much more powerful legalization/optimization passes, that would've been unfathomable2 for direct SPIR-V manipulation. 25 | 26 | --- 27 | 28 | 1 Rust is not unique in its needs here, and more languages (or IRs) could eventually make use of such a framework, but the initial design and implementation work has focused on [Rust-GPU] 29 | 30 | 2 not outright impossible, but requiring excessive development/maintenance cost, having to constantly balance correctness and power (more conservative passes are easier to trust), etc. 31 | 32 | ## Disclaimer 33 | 34 | This project is not affiliated, associated, authorized, endorsed by, or in any way officially connected with Khronos Group Inc., or any of its subsidiaries or its affiliates. The official Khronos Group Inc. website can be found at https://www.khronos.org. 35 | The names SPIR, SPIR-V, as well as related names, marks, emblems and images are trademarks of their respective owners. 36 | 37 | Additional context: the name of this project is a pun on SPIR-V, and entirely unrelated to SPIR (the older IR standard). 38 | 39 | ## Status 40 | 41 | 🚧 *This project is in active design and development, many details can and will change* 🚧 42 | 43 | If you're interested in using **SPIR-🇹** yourself, you may want to first take a look at [the issue tracker](https://github.com/EmbarkStudios/spirt/issues) for relevant issues, and even open new ones describing your usecase. 44 | With the initial focus being on [Rust-GPU]'s usecase, various (otherwise desirable) functionality/APIs/docs may be lacking, or rapidly changing - at the same time, discussions around widening the scope and usability of **SPIR-🇹** _in the long term_ are still welcome. 45 | 46 | ### Non-goals (at least in the short term) 47 | 48 | * supporting the ("OpenCL") `Kernel` dialect of SPIR-V 49 | * `Kernel` SPIR-V is much closer to LLVM IR, than `Shader` SPIR-V, and 50 | as such tooling oriented around LLVM is more likely to be a better fit 51 | * textual syntax that can be parsed back 52 | * i.e. the pretty-printer output is purely a visualization 53 | 54 | ### Designed and implemented so far 55 | 56 | 57 |
58 | 59 | **IR data types**: 60 | * allowing near-arbitrary SPIR-V instructions for any unrecognized opcodes 61 | * IDs are replaced with interned/"entity" handles (see below) 62 | * interning for attributes (decorations & similar), types and constants 63 | * i.e. automatic deduplication, efficient indexing, and no concept of "definition" 64 | (only uses of interned handles can lead to a module being considered to contain a specific type/constant) 65 | * "entity" system for e.g. definitions in a module, instructions in a function, etc. 66 | * disallows iteration in favor of/forcing the use of efficient indexing 67 | * structured control-flow "regions" inspired by RVSDG, stricter than SPIR-V 68 | (see `ControlRegionDef`'s docs for more details) 69 | 70 | 71 | 72 | **Framework utilities**: 73 | * `visit`/`transform`: immutable/mutable IR traversal 74 | * `print`: pretty-printer with (styled and hyperlinked) HTML output 75 | 76 | **Passes (to/from/on SPIR-🇹)**: 77 | * `spv::lower`: "lowering" from SPIR-V, normalizing away many irrelevant details 78 | * lossy for some relevant details (these are bugs, though many are non-semantic so lower priority) 79 | * `spv::lift`: "lifting" back up to SPIR-V, making arbitrary choices where necessary 80 | * comparable to e.g. generating GLSL syntax from SPIR-V, just one level down 81 | * `cfg::Structurizer`: (re)structurization, from arbitrary control-flow to the stricter structured "regions" 82 | * `passes::link`: mapping (linkage) imports to relevant exports 83 | 84 |
85 | 86 | ## Simple example (with non-trivial control-flow) 87 | 88 | 89 |
90 | 91 |
92 | 93 | **GLSL** ([`for-loop.vert.glsl`](tests/data/for-loop.vert.glsl))
94 | 95 | 96 | ```glsl 97 | #version 450 98 | out int output0; 99 | void main() { 100 | int o = 1; 101 | for(int i = 1; i < 10; i++) 102 | o *= i; 103 | output0 = o; 104 | } 105 | ``` 106 | 107 |
108 | 109 | **WGSL** ([`for-loop.wgsl`](tests/data/for-loop.wgsl))
110 | 111 | 112 | 113 | ```glsl 114 | @vertex 115 | fn main() -> @location(0) i32 { 116 | var o: i32 = 1; 117 | for(var i: i32 = 1; i < 10; i++) { 118 | o *= i; 119 | } 120 | return o; 121 | } 122 | ``` 123 | 124 |
125 | 126 | 127 |
128 | 129 | **SPIR-🇹**
130 | 131 | 132 | 133 | 134 | 135 | ```cxx 136 | #[spv.Decoration.Flat] 137 | #[spv.Decoration.Location(Location: 0)] 138 | global_var GV0 in spv.StorageClass.Output: s32 139 | 140 | func F0() -> spv.OpTypeVoid { 141 | loop(v0: s32 <- 1s32, v1: s32 <- 1s32) { 142 | v2 = spv.OpSLessThan(v1, 10s32): bool 143 | (v3: s32, v4: s32) = if v2 { 144 | v5 = spv.OpIMul(v0, v1): s32 145 | v6 = spv.OpIAdd(v1, 1s32): s32 146 | (v5, v6) 147 | } else { 148 | (spv.OpUndef: s32, spv.OpUndef: s32) 149 | } 150 | (v3, v4) -> (v0, v1) 151 | } while v2 152 | spv.OpStore(Pointer: &GV0, Object: v0) 153 | } 154 | ``` 155 | 156 | 157 |
158 | 159 |
160 | 161 | **SPIR-V** ([`for-loop.wgsl.spvasm`](tests/data/for-loop.wgsl.spvasm))
162 | 163 | 164 | 165 | ```llvm 166 | %typeof_output0 = OpTypePointer Output %i32 167 | %output0 = OpVariable %typeof_output0 Output 168 | 169 | %typeof_main = OpTypeFunction %void 170 | %main = OpFunction %void None %typeof_main 171 | %entry = OpLabel 172 | OpBranch %bb0 173 | %bb0 = OpLabel 174 | OpBranch %bb1 175 | %bb1 = OpLabel 176 | %o = OpPhi %i32 %1_i32 %bb0 %o_next %bb5 177 | %i = OpPhi %i32 %0_i32 %bb0 %i_next %bb5 178 | OpLoopMerge %bb6 %bb5 None 179 | OpBranch %bb2 180 | %bb2 = OpLabel 181 | %cond = OpSLessThan %bool %i %10_i32 182 | OpSelectionMerge %bb4 None 183 | OpBranchConditional %cond %bb4 %bb3 184 | %bb3 = OpLabel 185 | OpBranch %bb6 186 | %bb4 = OpLabel 187 | %o_next = OpIMul %i32 %o %i 188 | OpBranch %bb5 189 | %bb5 = OpLabel 190 | %i_next = OpIAdd %i32 %i %1_i32 191 | OpBranch %bb1 192 | %bb6 = OpLabel 193 | OpStore %output0 %o 194 | OpReturn 195 | OpFunctionEnd 196 | ``` 197 | 198 |
199 | 200 | ## GPU (shader) IR landscape overview 201 | *(and the vision of how **SPIR-🇹** fits into it)* 202 | 203 | ![](docs/landscape.svg) 204 | 205 | The distinction being made here is between: 206 | * **Interchange IRs** (standards that many tools can use to interoperate) 207 | * SPIR-V was very much intended as such a standard 208 | (outside of the GPU space, wasm is also a great example) 209 | * they only need to encode the right concepts, not straying too far away from what tools understand, but the design effort is often oriented around being a "serialization" format 210 | * **Compiler IRs** (non-standard implementation details of compilers) 211 | * LLVM is quite well-known, but Mesa's NIR is even closer to **SPIR-🇹** 212 | (both being shader-oriented, and having similar specialized choices of e.g. handling control-flow) 213 | * these _have to_ handle legalization/optimization passes quite well, and in general a lot of on-the-fly transformations - as their main purpose is to _expedite_ such operations 214 | * this is where **SPIR-🇹** sits, as a kind of "relative"/dialect of SPIR-V, but making trade-offs in favor of the "intra-compiler" usage 215 | 216 | ## Contribution 217 | 218 | [![Contributor Covenant](https://img.shields.io/badge/contributor%20covenant-v1.4-ff69b4.svg)](CODE_OF_CONDUCT.md) 219 | 220 | We welcome community contributions to this project. 221 | 222 | Please read our [Contributor Guide](CONTRIBUTING.md) for more information on how to get started. 223 | Please also read our [Contributor Terms](CONTRIBUTING.md#contributor-terms) before you make any contributions. 224 | 225 | Any contribution intentionally submitted for inclusion in an Embark Studios project, shall comply with the Rust standard licensing model (MIT OR Apache 2.0) and therefore be dual licensed as described below, without any additional terms or conditions: 226 | 227 | ### License 228 | 229 | This contribution is dual licensed under EITHER OF 230 | 231 | - Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or ) 232 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or ) 233 | 234 | at your option. 235 | 236 | For clarity, "your" refers to Embark or any other licensee/user of the contribution. 237 | 238 | [Rust-GPU]: https://github.com/EmbarkStudios/rust-gpu 239 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/spv/print.rs: -------------------------------------------------------------------------------- 1 | //! Pretty-printing SPIR-V operands. 2 | 3 | use crate::spv::{self, spec}; 4 | use smallvec::SmallVec; 5 | use std::borrow::Cow; 6 | use std::fmt::Write; 7 | use std::{iter, mem, str}; 8 | 9 | /// The smallest unit produced by printing a ("logical") SPIR-V operand. 10 | /// 11 | /// All variants other than `Id` contain a fully formatted string, and the 12 | /// distinction between variants can be erased to obtain a plain-text version 13 | /// (also except `OperandKindNamespacePrefix` requiring an extra implicit `.`). 14 | // 15 | // FIXME(eddyb) should there be a `TokenKind` enum and then `Cow<'static, str>` 16 | // paired with a `TokenKind` in place of all of these individual variants? 17 | pub enum Token { 18 | /// An inconsistency was detected in the operands to be printed. 19 | /// For stylistic consistency, the error message is always found wrapped in 20 | /// a block comment (i.e. the [`String`] is always of the form `"/* ... */"`). 21 | Error(String), 22 | 23 | // NOTE(eddyb) this implies a suffix `: ` not included in the string, and 24 | // optionally some processing of the name (e.g. removing spaces). 25 | OperandName(&'static str), 26 | 27 | // FIXME(eddyb) perhaps encode the hierarchical structure of e.g. enumerand 28 | // parameters, so that the SPIR-T printer can do layout for them. 29 | Punctuation(&'static str), 30 | 31 | // NOTE(eddyb) this implies a suffix `.` not included in the string. 32 | OperandKindNamespacePrefix(&'static str), 33 | 34 | EnumerandName(&'static str), 35 | 36 | NumericLiteral(String), 37 | StringLiteral(String), 38 | 39 | /// Unprinted ID operand, of its original type (allowing post-processing). 40 | Id(ID), 41 | } 42 | 43 | /// All the [`Token`]s outputted by printing one single ("logical") SPIR-V operand, 44 | /// which may be concatenated (after separately processing `ID`s) to obtain a 45 | /// complete plain-text version of the printed operand. 46 | pub struct TokensForOperand { 47 | pub tokens: SmallVec<[Token; 3]>, 48 | } 49 | 50 | impl Default for TokensForOperand { 51 | fn default() -> Self { 52 | Self { tokens: SmallVec::new() } 53 | } 54 | } 55 | 56 | impl TokensForOperand { 57 | pub fn concat_to_plain_text(self) -> String { 58 | self.tokens 59 | .into_iter() 60 | .flat_map(|token| { 61 | let (first, second): (Cow<'_, str>, _) = match token { 62 | Token::OperandName(s) => (s.into(), Some(": ".into())), 63 | Token::OperandKindNamespacePrefix(s) => (s.into(), Some(".".into())), 64 | Token::Punctuation(s) | Token::EnumerandName(s) => (s.into(), None), 65 | Token::Error(s) 66 | | Token::NumericLiteral(s) 67 | | Token::StringLiteral(s) 68 | | Token::Id(s) => (s.into(), None), 69 | }; 70 | [first].into_iter().chain(second) 71 | }) 72 | .reduce(|out, extra| (out.into_owned() + &extra).into()) 73 | .unwrap_or_default() 74 | .into_owned() 75 | } 76 | } 77 | 78 | // FIXME(eddyb) keep a `&'static spec::Spec` if that can even speed up anything. 79 | struct OperandPrinter, ID, IDS: Iterator> { 80 | /// Input immediate operands to print from (may be grouped e.g. into literals). 81 | imms: iter::Peekable, 82 | 83 | /// Input ID operands to print from. 84 | ids: iter::Peekable, 85 | 86 | /// Output for the current operand (drained by the `inst_operands` method). 87 | out: TokensForOperand, 88 | } 89 | 90 | impl, ID, IDS: Iterator> OperandPrinter { 91 | fn is_exhausted(&mut self) -> bool { 92 | self.imms.peek().is_none() && self.ids.peek().is_none() 93 | } 94 | 95 | fn enumerant_params(&mut self, enumerant: &spec::Enumerant) { 96 | let mut first = true; 97 | for (mode, name_and_kind) in enumerant.all_params_with_names() { 98 | if mode == spec::OperandMode::Optional && self.is_exhausted() { 99 | break; 100 | } 101 | 102 | self.out.tokens.push(Token::Punctuation(if first { "(" } else { ", " })); 103 | first = false; 104 | 105 | let (name, kind) = name_and_kind.name_and_kind(); 106 | self.operand(name, kind); 107 | } 108 | if !first { 109 | self.out.tokens.push(Token::Punctuation(")")); 110 | } 111 | } 112 | 113 | fn literal(&mut self, kind: spec::OperandKind, first_word: u32) { 114 | // HACK(eddyb) easier to buffer these than to deal with iterators. 115 | let mut words = SmallVec::<[u32; 16]>::new(); 116 | words.push(first_word); 117 | while let Some(&spv::Imm::LongCont(cont_kind, word)) = self.imms.peek() { 118 | self.imms.next(); 119 | assert_eq!(kind, cont_kind); 120 | words.push(word); 121 | } 122 | 123 | let def = kind.def(); 124 | assert!(matches!(def, spec::OperandKindDef::Literal { .. })); 125 | 126 | let literal_token = if kind == spec::Spec::get().well_known.LiteralString { 127 | // FIXME(eddyb) deduplicate with `spv::extract_literal_string`. 128 | let bytes: SmallVec<[u8; 64]> = words 129 | .into_iter() 130 | .flat_map(u32::to_le_bytes) 131 | .take_while(|&byte| byte != 0) 132 | .collect(); 133 | match str::from_utf8(&bytes) { 134 | Ok(s) => Token::StringLiteral(format!("{s:?}")), 135 | Err(e) => Token::Error(format!("/* {e} in {bytes:?} */")), 136 | } 137 | } else { 138 | let mut words_msb_to_lsb = 139 | words.into_iter().rev().skip_while(|&word| word == 0).peekable(); 140 | let most_significant_word = words_msb_to_lsb.next().unwrap_or(0); 141 | 142 | // FIXME(eddyb) use a more advanced decision procedure for picking 143 | // how to print integer(?) literals. 144 | let mut s; 145 | if words_msb_to_lsb.peek().is_none() && most_significant_word <= 0xffff { 146 | s = format!("{most_significant_word}"); 147 | } else { 148 | s = format!("0x{most_significant_word:x}"); 149 | for word in words_msb_to_lsb { 150 | write!(s, "_{word:08x}").unwrap(); 151 | } 152 | } 153 | Token::NumericLiteral(s) 154 | }; 155 | 156 | self.out.tokens.push(literal_token); 157 | } 158 | 159 | fn operand(&mut self, operand_name: &'static str, kind: spec::OperandKind) { 160 | if !operand_name.is_empty() { 161 | self.out.tokens.push(Token::OperandName(operand_name)); 162 | } 163 | 164 | let (name, def) = kind.name_and_def(); 165 | 166 | // FIXME(eddyb) should this be a hard error? 167 | let emit_missing_error = |this: &mut Self| { 168 | this.out.tokens.push(Token::Error(format!("/* missing {name} */"))); 169 | }; 170 | 171 | let mut maybe_get_enum_word = || match self.imms.next() { 172 | Some(spv::Imm::Short(found_kind, word)) => { 173 | assert_eq!(kind, found_kind); 174 | Some(word) 175 | } 176 | Some(spv::Imm::LongStart(..) | spv::Imm::LongCont(..)) => unreachable!(), 177 | None => None, 178 | }; 179 | 180 | match def { 181 | spec::OperandKindDef::BitEnum { empty_name, bits } => { 182 | let word = match maybe_get_enum_word() { 183 | Some(word) => word, 184 | None => return emit_missing_error(self), 185 | }; 186 | 187 | self.out.tokens.push(Token::OperandKindNamespacePrefix(name)); 188 | if word == 0 { 189 | self.out.tokens.push(Token::EnumerandName(empty_name)); 190 | } else if let Some(bit_idx) = spec::BitIdx::of_single_set_bit(word) { 191 | let (bit_name, bit_def) = bits.get_named(bit_idx).unwrap(); 192 | self.out.tokens.push(Token::EnumerandName(bit_name)); 193 | self.enumerant_params(bit_def); 194 | } else { 195 | self.out.tokens.push(Token::Punctuation("{")); 196 | let mut first = true; 197 | for bit_idx in spec::BitIdx::of_all_set_bits(word) { 198 | if !first { 199 | self.out.tokens.push(Token::Punctuation(", ")); 200 | } 201 | first = false; 202 | 203 | let (bit_name, bit_def) = bits.get_named(bit_idx).unwrap(); 204 | self.out.tokens.push(Token::EnumerandName(bit_name)); 205 | self.enumerant_params(bit_def); 206 | } 207 | self.out.tokens.push(Token::Punctuation("}")); 208 | } 209 | } 210 | spec::OperandKindDef::ValueEnum { variants } => { 211 | let word = match maybe_get_enum_word() { 212 | Some(word) => word, 213 | None => return emit_missing_error(self), 214 | }; 215 | 216 | let (variant_name, variant_def) = 217 | variants.get_named(word.try_into().unwrap()).unwrap(); 218 | self.out.tokens.extend([ 219 | Token::OperandKindNamespacePrefix(name), 220 | Token::EnumerandName(variant_name), 221 | ]); 222 | self.enumerant_params(variant_def); 223 | } 224 | spec::OperandKindDef::Id => match self.ids.next() { 225 | Some(id) => { 226 | self.out.tokens.push(Token::Id(id)); 227 | } 228 | None => emit_missing_error(self), 229 | }, 230 | spec::OperandKindDef::Literal { .. } => { 231 | // FIXME(eddyb) there's no reason to take the first word now, 232 | // `self.literal(kind)` could do it itself. 233 | match self.imms.next() { 234 | Some( 235 | spv::Imm::Short(found_kind, word) | spv::Imm::LongStart(found_kind, word), 236 | ) => { 237 | assert_eq!(kind, found_kind); 238 | self.literal(kind, word); 239 | } 240 | Some(spv::Imm::LongCont(..)) => unreachable!(), 241 | None => emit_missing_error(self), 242 | } 243 | } 244 | } 245 | } 246 | 247 | fn inst_operands(mut self, opcode: spec::Opcode) -> impl Iterator> { 248 | opcode.def().all_operands_with_names().map_while(move |(mode, name_and_kind)| { 249 | if mode == spec::OperandMode::Optional && self.is_exhausted() { 250 | return None; 251 | } 252 | let (name, kind) = name_and_kind.name_and_kind(); 253 | self.operand(name, kind); 254 | Some(mem::take(&mut self.out)) 255 | }) 256 | } 257 | } 258 | 259 | /// Print a single SPIR-V operand from only immediates, potentially composed of 260 | /// an enumerand with parameters (which consumes more immediates). 261 | pub fn operand_from_imms(imms: impl IntoIterator) -> TokensForOperand { 262 | let mut printer = OperandPrinter { 263 | imms: imms.into_iter().peekable(), 264 | ids: iter::empty().peekable(), 265 | out: TokensForOperand::default(), 266 | }; 267 | let &kind = match printer.imms.peek().unwrap() { 268 | spv::Imm::Short(kind, _) | spv::Imm::LongStart(kind, _) => kind, 269 | spv::Imm::LongCont(..) => unreachable!(), 270 | }; 271 | printer.operand("", kind); 272 | assert!(printer.imms.next().is_none()); 273 | printer.out 274 | } 275 | 276 | /// Group (ordered according to `opcode`) `imms` and `ids` into logical operands 277 | /// (i.e. long immediates are unflattened) and produce one [`TokensForOperand`] by 278 | /// printing each of them. 279 | pub fn inst_operands( 280 | opcode: spec::Opcode, 281 | imms: impl IntoIterator, 282 | ids: impl IntoIterator, 283 | ) -> impl Iterator> { 284 | OperandPrinter { 285 | imms: imms.into_iter().peekable(), 286 | ids: ids.into_iter().peekable(), 287 | out: TokensForOperand::default(), 288 | } 289 | .inst_operands(opcode) 290 | } 291 | -------------------------------------------------------------------------------- /src/passes/link.rs: -------------------------------------------------------------------------------- 1 | use crate::transform::{InnerTransform, Transformed, Transformer}; 2 | use crate::visit::{InnerVisit, Visitor}; 3 | use crate::{ 4 | AttrSet, Const, Context, DataInstForm, DeclDef, ExportKey, Exportee, Func, FxIndexSet, 5 | GlobalVar, Import, Module, Type, 6 | }; 7 | use rustc_hash::{FxHashMap, FxHashSet}; 8 | use std::collections::VecDeque; 9 | 10 | // FIXME(eddyb) maybe make an export pruning pass that keeps some exports as 11 | // roots and then only other exports if they're used by imports. 12 | 13 | /// Remove exports which aren't "roots" (`is_root(export_key)` returns `false`), 14 | /// and which aren't otherwise kept alive by a "root" (through [`Import::LinkName`] 15 | /// declarations, with `name` matching [`ExportKey::LinkName`]), either directly 16 | /// or transitively (including through any number of imports). 17 | /// 18 | /// In essence, other than the "root" exports, `minimize_exports` only keeps the 19 | /// exports that `resolve_imports` would use, and is recommended to first call 20 | /// `minimize_exports` before using [`resolve_imports`], to reduce work. 21 | /// 22 | /// Note that the "dead" definitions are not removed from the module, and any 23 | /// external references to them could still be used (e.g. from a clone of the 24 | /// `module.exports` map, before calling `minimize_exports`). 25 | // 26 | // FIXME(eddyb) make this operate on multiple modules. 27 | pub fn minimize_exports(module: &mut Module, is_root: impl Fn(&ExportKey) -> bool) { 28 | let mut collector = LiveExportCollector { 29 | cx: module.cx_ref(), 30 | module, 31 | 32 | live_exports: FxIndexSet::default(), 33 | 34 | seen_types: FxHashSet::default(), 35 | seen_consts: FxHashSet::default(), 36 | seen_data_inst_forms: FxHashSet::default(), 37 | seen_global_vars: FxHashSet::default(), 38 | seen_funcs: FxHashSet::default(), 39 | }; 40 | for (export_key, &exportee) in &module.exports { 41 | if is_root(export_key) && collector.live_exports.insert(export_key.clone()) { 42 | exportee.inner_visit_with(&mut collector); 43 | } 44 | } 45 | module.exports = collector 46 | .live_exports 47 | .into_iter() 48 | .map(|export_key| { 49 | let exportee = module.exports[&export_key]; 50 | (export_key, exportee) 51 | }) 52 | .collect(); 53 | } 54 | 55 | struct LiveExportCollector<'a> { 56 | cx: &'a Context, 57 | module: &'a Module, 58 | 59 | live_exports: FxIndexSet, 60 | 61 | // FIXME(eddyb) build some automation to avoid ever repeating these. 62 | seen_types: FxHashSet, 63 | seen_consts: FxHashSet, 64 | seen_data_inst_forms: FxHashSet, 65 | seen_global_vars: FxHashSet, 66 | seen_funcs: FxHashSet, 67 | } 68 | 69 | impl Visitor<'_> for LiveExportCollector<'_> { 70 | // FIXME(eddyb) build some automation to avoid ever repeating these. 71 | fn visit_attr_set_use(&mut self, _attrs: AttrSet) { 72 | // FIXME(eddyb) if `AttrSet`s are ignored, why not `Type`s too? 73 | } 74 | fn visit_type_use(&mut self, ty: Type) { 75 | if self.seen_types.insert(ty) { 76 | self.visit_type_def(&self.cx[ty]); 77 | } 78 | } 79 | fn visit_const_use(&mut self, ct: Const) { 80 | if self.seen_consts.insert(ct) { 81 | self.visit_const_def(&self.cx[ct]); 82 | } 83 | } 84 | fn visit_data_inst_form_use(&mut self, data_inst_form: DataInstForm) { 85 | if self.seen_data_inst_forms.insert(data_inst_form) { 86 | self.visit_data_inst_form_def(&self.cx[data_inst_form]); 87 | } 88 | } 89 | 90 | fn visit_global_var_use(&mut self, gv: GlobalVar) { 91 | if self.seen_global_vars.insert(gv) { 92 | self.visit_global_var_decl(&self.module.global_vars[gv]); 93 | } 94 | } 95 | fn visit_func_use(&mut self, func: Func) { 96 | if self.seen_funcs.insert(func) { 97 | self.visit_func_decl(&self.module.funcs[func]); 98 | } 99 | } 100 | 101 | fn visit_import(&mut self, import: &Import) { 102 | match *import { 103 | Import::LinkName(name) => { 104 | let export_key = ExportKey::LinkName(name); 105 | if let Some(&exportee) = self.module.exports.get(&export_key) { 106 | if self.live_exports.insert(export_key) { 107 | exportee.inner_visit_with(self); 108 | } 109 | } 110 | } 111 | } 112 | } 113 | } 114 | 115 | /// Remap [`Import::LinkName`] to definitions exported as [`ExportKey::LinkName`]. 116 | /// 117 | /// To reduce the work performed, calling [`minimize_exports`] first is recommended. 118 | // 119 | // FIXME(eddyb) make this operate on multiple modules. 120 | pub fn resolve_imports(module: &mut Module) { 121 | let (resolved_global_vars, resolved_funcs) = { 122 | let mut collector = ImportResolutionCollector { 123 | cx: module.cx_ref(), 124 | module, 125 | 126 | resolved_global_vars: FxHashMap::default(), 127 | resolved_funcs: FxHashMap::default(), 128 | 129 | seen_types: FxHashSet::default(), 130 | seen_consts: FxHashSet::default(), 131 | seen_data_inst_forms: FxHashSet::default(), 132 | seen_global_vars: FxHashSet::default(), 133 | seen_funcs: FxHashSet::default(), 134 | }; 135 | collector.visit_module(module); 136 | (collector.resolved_global_vars, collector.resolved_funcs) 137 | }; 138 | 139 | let mut resolver = ImportResolver { 140 | cx: &module.cx(), 141 | 142 | resolved_global_vars: &resolved_global_vars, 143 | resolved_funcs: &resolved_funcs, 144 | 145 | transformed_types: FxHashMap::default(), 146 | transformed_consts: FxHashMap::default(), 147 | transformed_data_inst_forms: FxHashMap::default(), 148 | transformed_global_vars: FxHashMap::default(), 149 | global_var_queue: VecDeque::new(), 150 | transformed_funcs: FxHashMap::default(), 151 | func_queue: VecDeque::new(), 152 | }; 153 | 154 | // Seed the queues starting from the module exports. 155 | for exportee in module.exports.values_mut() { 156 | exportee.inner_transform_with(&mut resolver).apply_to(exportee); 157 | } 158 | 159 | // Process the queues until they're all empty. 160 | while !resolver.global_var_queue.is_empty() || !resolver.func_queue.is_empty() { 161 | while let Some(gv) = resolver.global_var_queue.pop_front() { 162 | resolver.in_place_transform_global_var_decl(&mut module.global_vars[gv]); 163 | } 164 | while let Some(func) = resolver.func_queue.pop_front() { 165 | resolver.in_place_transform_func_decl(&mut module.funcs[func]); 166 | } 167 | } 168 | } 169 | 170 | // FIXME(eddyb) figure out if this step can be skipped by somehow letting 171 | // `ImportResolver` access declarations while mutating definitions. 172 | struct ImportResolutionCollector<'a> { 173 | cx: &'a Context, 174 | module: &'a Module, 175 | 176 | resolved_global_vars: FxHashMap, 177 | resolved_funcs: FxHashMap, 178 | 179 | // FIXME(eddyb) build some automation to avoid ever repeating these. 180 | seen_types: FxHashSet, 181 | seen_consts: FxHashSet, 182 | seen_data_inst_forms: FxHashSet, 183 | seen_global_vars: FxHashSet, 184 | seen_funcs: FxHashSet, 185 | } 186 | 187 | impl Visitor<'_> for ImportResolutionCollector<'_> { 188 | // FIXME(eddyb) build some automation to avoid ever repeating these. 189 | fn visit_attr_set_use(&mut self, _attrs: AttrSet) { 190 | // FIXME(eddyb) if `AttrSet`s are ignored, why not `Type`s too? 191 | } 192 | fn visit_type_use(&mut self, ty: Type) { 193 | if self.seen_types.insert(ty) { 194 | self.visit_type_def(&self.cx[ty]); 195 | } 196 | } 197 | fn visit_const_use(&mut self, ct: Const) { 198 | if self.seen_consts.insert(ct) { 199 | self.visit_const_def(&self.cx[ct]); 200 | } 201 | } 202 | fn visit_data_inst_form_use(&mut self, data_inst_form: DataInstForm) { 203 | if self.seen_data_inst_forms.insert(data_inst_form) { 204 | self.visit_data_inst_form_def(&self.cx[data_inst_form]); 205 | } 206 | } 207 | 208 | fn visit_global_var_use(&mut self, gv: GlobalVar) { 209 | if self.seen_global_vars.insert(gv) { 210 | let gv_decl = &self.module.global_vars[gv]; 211 | self.visit_global_var_decl(gv_decl); 212 | 213 | // FIXME(eddyb) if the export is missing (or the wrong kind), it will 214 | // simply not get remapped - perhaps some kind of diagnostic is in 215 | // order? (maybe an entire pass infrastructure that can report errors) 216 | if let DeclDef::Imported(Import::LinkName(name)) = gv_decl.def { 217 | if let Some(&Exportee::GlobalVar(def_gv)) = 218 | self.module.exports.get(&ExportKey::LinkName(name)) 219 | { 220 | self.resolved_global_vars.insert(gv, def_gv); 221 | } 222 | } 223 | } 224 | } 225 | fn visit_func_use(&mut self, func: Func) { 226 | if self.seen_funcs.insert(func) { 227 | let func_decl = &self.module.funcs[func]; 228 | self.visit_func_decl(func_decl); 229 | 230 | // FIXME(eddyb) if the export is missing (or the wrong kind), it will 231 | // simply not get remapped - perhaps some kind of diagnostic is in 232 | // order? (maybe an entire pass infrastructure that can report errors) 233 | if let DeclDef::Imported(Import::LinkName(name)) = func_decl.def { 234 | if let Some(&Exportee::Func(def_func)) = 235 | self.module.exports.get(&ExportKey::LinkName(name)) 236 | { 237 | self.resolved_funcs.insert(func, def_func); 238 | } 239 | } 240 | } 241 | } 242 | } 243 | 244 | struct ImportResolver<'a> { 245 | cx: &'a Context, 246 | 247 | resolved_global_vars: &'a FxHashMap, 248 | resolved_funcs: &'a FxHashMap, 249 | 250 | // FIXME(eddyb) build some automation to avoid ever repeating these. 251 | transformed_types: FxHashMap>, 252 | transformed_consts: FxHashMap>, 253 | transformed_data_inst_forms: FxHashMap>, 254 | transformed_global_vars: FxHashMap>, 255 | global_var_queue: VecDeque, 256 | transformed_funcs: FxHashMap>, 257 | func_queue: VecDeque, 258 | } 259 | 260 | impl Transformer for ImportResolver<'_> { 261 | // FIXME(eddyb) build some automation to avoid ever repeating these. 262 | fn transform_type_use(&mut self, ty: Type) -> Transformed { 263 | if let Some(&cached) = self.transformed_types.get(&ty) { 264 | return cached; 265 | } 266 | let transformed = 267 | self.transform_type_def(&self.cx[ty]).map(|ty_def| self.cx.intern(ty_def)); 268 | self.transformed_types.insert(ty, transformed); 269 | transformed 270 | } 271 | fn transform_const_use(&mut self, ct: Const) -> Transformed { 272 | if let Some(&cached) = self.transformed_consts.get(&ct) { 273 | return cached; 274 | } 275 | let transformed = 276 | self.transform_const_def(&self.cx[ct]).map(|ct_def| self.cx.intern(ct_def)); 277 | self.transformed_consts.insert(ct, transformed); 278 | transformed 279 | } 280 | fn transform_data_inst_form_use( 281 | &mut self, 282 | data_inst_form: DataInstForm, 283 | ) -> Transformed { 284 | if let Some(&cached) = self.transformed_data_inst_forms.get(&data_inst_form) { 285 | return cached; 286 | } 287 | let transformed = self 288 | .transform_data_inst_form_def(&self.cx[data_inst_form]) 289 | .map(|data_inst_form_def| self.cx.intern(data_inst_form_def)); 290 | self.transformed_data_inst_forms.insert(data_inst_form, transformed); 291 | transformed 292 | } 293 | 294 | fn transform_global_var_use(&mut self, gv: GlobalVar) -> Transformed { 295 | if let Some(&cached) = self.transformed_global_vars.get(&gv) { 296 | return cached; 297 | } 298 | let transformed = match self.resolved_global_vars.get(&gv).copied() { 299 | Some(mut new_gv) => { 300 | self.transform_global_var_use(new_gv).apply_to(&mut new_gv); 301 | Transformed::Changed(new_gv) 302 | } 303 | None => { 304 | self.global_var_queue.push_back(gv); 305 | Transformed::Unchanged 306 | } 307 | }; 308 | self.transformed_global_vars.insert(gv, transformed); 309 | transformed 310 | } 311 | fn transform_func_use(&mut self, func: Func) -> Transformed { 312 | if let Some(&cached) = self.transformed_funcs.get(&func) { 313 | return cached; 314 | } 315 | let transformed = match self.resolved_funcs.get(&func).copied() { 316 | Some(mut new_func) => { 317 | self.transform_func_use(new_func).apply_to(&mut new_func); 318 | Transformed::Changed(new_func) 319 | } 320 | None => { 321 | self.func_queue.push_back(func); 322 | Transformed::Unchanged 323 | } 324 | }; 325 | self.transformed_funcs.insert(func, transformed); 326 | transformed 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /src/spv/read.rs: -------------------------------------------------------------------------------- 1 | //! Low-level parsing of SPIR-V binary form. 2 | 3 | use crate::spv::{self, spec}; 4 | use rustc_hash::FxHashMap; 5 | use smallvec::SmallVec; 6 | use std::borrow::Cow; 7 | use std::num::NonZeroU32; 8 | use std::path::Path; 9 | use std::{fs, io, iter, slice}; 10 | 11 | /// Defining instruction of an ID. 12 | /// 13 | /// Used currently only to help parsing `LiteralContextDependentNumber`. 14 | enum KnownIdDef { 15 | TypeInt(NonZeroU32), 16 | TypeFloat(NonZeroU32), 17 | Uncategorized { opcode: spec::Opcode, result_type_id: Option }, 18 | } 19 | 20 | impl KnownIdDef { 21 | fn result_type_id(&self) -> Option { 22 | match *self { 23 | Self::TypeInt(_) | Self::TypeFloat(_) => None, 24 | Self::Uncategorized { result_type_id, .. } => result_type_id, 25 | } 26 | } 27 | } 28 | 29 | // FIXME(eddyb) keep a `&'static spec::Spec` if that can even speed up anything. 30 | struct InstParser<'a> { 31 | /// IDs defined so far in the module. 32 | known_ids: &'a FxHashMap, 33 | 34 | /// Input words of an instruction. 35 | words: iter::Copied>, 36 | 37 | /// Output instruction, being parsed. 38 | inst: spv::InstWithIds, 39 | } 40 | 41 | enum InstParseError { 42 | /// Ran out of words while parsing an instruction's operands. 43 | NotEnoughWords, 44 | 45 | /// Extra words were left over, after parsing an instruction's operands. 46 | TooManyWords, 47 | 48 | /// An illegal ID of `0`. 49 | IdZero, 50 | 51 | /// Unsupported enumerand value. 52 | UnsupportedEnumerand(spec::OperandKind, u32), 53 | 54 | /// An `IdResultType` ID referring to an ID not already defined. 55 | UnknownResultTypeId(spv::Id), 56 | 57 | /// The type of a `LiteralContextDependentNumber` could not be determined. 58 | MissingContextSensitiveLiteralType, 59 | 60 | /// The type of a `LiteralContextDependentNumber` was not a supported type 61 | /// (one of either `OpTypeInt` or `OpTypeFloat`). 62 | UnsupportedContextSensitiveLiteralType { type_opcode: spec::Opcode }, 63 | } 64 | 65 | impl InstParseError { 66 | // FIXME(eddyb) improve messages and add more contextual information. 67 | fn message(&self) -> Cow<'static, str> { 68 | match *self { 69 | Self::NotEnoughWords => "truncated instruction".into(), 70 | Self::TooManyWords => "overlong instruction".into(), 71 | Self::IdZero => "ID %0 is illegal".into(), 72 | // FIXME(eddyb) deduplicate this with `spv::write`. 73 | Self::UnsupportedEnumerand(kind, word) => { 74 | let (name, def) = kind.name_and_def(); 75 | match def { 76 | spec::OperandKindDef::BitEnum { bits, .. } => { 77 | let unsupported = spec::BitIdx::of_all_set_bits(word) 78 | .filter(|&bit_idx| bits.get(bit_idx).is_none()) 79 | .fold(0u32, |x, i| x | (1 << i.0)); 80 | format!("unsupported {name} bit-pattern 0x{unsupported:08x}").into() 81 | } 82 | 83 | spec::OperandKindDef::ValueEnum { .. } => { 84 | format!("unsupported {name} value {word}").into() 85 | } 86 | 87 | _ => unreachable!(), 88 | } 89 | } 90 | Self::UnknownResultTypeId(id) => { 91 | format!("ID %{id} used as result type before definition").into() 92 | } 93 | Self::MissingContextSensitiveLiteralType => "missing type for literal".into(), 94 | Self::UnsupportedContextSensitiveLiteralType { type_opcode } => { 95 | format!("{} is not a supported literal type", type_opcode.name()).into() 96 | } 97 | } 98 | } 99 | } 100 | 101 | impl InstParser<'_> { 102 | fn is_exhausted(&self) -> bool { 103 | // FIXME(eddyb) use `self.words.is_empty()` when that is stabilized. 104 | self.words.len() == 0 105 | } 106 | 107 | fn enumerant_params(&mut self, enumerant: &spec::Enumerant) -> Result<(), InstParseError> { 108 | for (mode, kind) in enumerant.all_params() { 109 | if mode == spec::OperandMode::Optional && self.is_exhausted() { 110 | break; 111 | } 112 | self.operand(kind)?; 113 | } 114 | 115 | Ok(()) 116 | } 117 | 118 | fn operand(&mut self, kind: spec::OperandKind) -> Result<(), InstParseError> { 119 | use InstParseError as Error; 120 | 121 | let word = self.words.next().ok_or(Error::NotEnoughWords)?; 122 | match kind.def() { 123 | spec::OperandKindDef::BitEnum { bits, .. } => { 124 | self.inst.imms.push(spv::Imm::Short(kind, word)); 125 | 126 | for bit_idx in spec::BitIdx::of_all_set_bits(word) { 127 | let bit_def = 128 | bits.get(bit_idx).ok_or(Error::UnsupportedEnumerand(kind, word))?; 129 | self.enumerant_params(bit_def)?; 130 | } 131 | } 132 | 133 | spec::OperandKindDef::ValueEnum { variants } => { 134 | self.inst.imms.push(spv::Imm::Short(kind, word)); 135 | 136 | let variant_def = u16::try_from(word) 137 | .ok() 138 | .and_then(|v| variants.get(v)) 139 | .ok_or(Error::UnsupportedEnumerand(kind, word))?; 140 | self.enumerant_params(variant_def)?; 141 | } 142 | 143 | spec::OperandKindDef::Id => { 144 | let id = word.try_into().ok().ok_or(Error::IdZero)?; 145 | self.inst.ids.push(id); 146 | } 147 | 148 | spec::OperandKindDef::Literal { size: spec::LiteralSize::Word } => { 149 | self.inst.imms.push(spv::Imm::Short(kind, word)); 150 | } 151 | spec::OperandKindDef::Literal { size: spec::LiteralSize::NulTerminated } => { 152 | let has_nul = |word: u32| word.to_le_bytes().contains(&0); 153 | if has_nul(word) { 154 | self.inst.imms.push(spv::Imm::Short(kind, word)); 155 | } else { 156 | self.inst.imms.push(spv::Imm::LongStart(kind, word)); 157 | for word in &mut self.words { 158 | self.inst.imms.push(spv::Imm::LongCont(kind, word)); 159 | if has_nul(word) { 160 | break; 161 | } 162 | } 163 | } 164 | } 165 | spec::OperandKindDef::Literal { size: spec::LiteralSize::FromContextualType } => { 166 | let contextual_type = self 167 | .inst 168 | .result_type_id 169 | .or_else(|| { 170 | // `OpSwitch` takes its literal type from the first operand. 171 | let &id = self.inst.ids.first()?; 172 | self.known_ids.get(&id)?.result_type_id() 173 | }) 174 | .and_then(|id| self.known_ids.get(&id)) 175 | .ok_or(Error::MissingContextSensitiveLiteralType)?; 176 | 177 | let extra_word_count = match *contextual_type { 178 | KnownIdDef::TypeInt(width) | KnownIdDef::TypeFloat(width) => { 179 | // HACK(eddyb) `(width + 31) / 32 - 1` but without overflow. 180 | (width.get() - 1) / 32 181 | } 182 | KnownIdDef::Uncategorized { opcode, .. } => { 183 | return Err(Error::UnsupportedContextSensitiveLiteralType { 184 | type_opcode: opcode, 185 | }); 186 | } 187 | }; 188 | 189 | if extra_word_count == 0 { 190 | self.inst.imms.push(spv::Imm::Short(kind, word)); 191 | } else { 192 | self.inst.imms.push(spv::Imm::LongStart(kind, word)); 193 | for _ in 0..extra_word_count { 194 | let word = self.words.next().ok_or(Error::NotEnoughWords)?; 195 | self.inst.imms.push(spv::Imm::LongCont(kind, word)); 196 | } 197 | } 198 | } 199 | } 200 | 201 | Ok(()) 202 | } 203 | 204 | fn inst(mut self, def: &spec::InstructionDef) -> Result { 205 | use InstParseError as Error; 206 | 207 | { 208 | // FIXME(eddyb) should this be a method? 209 | let mut id = || { 210 | self.words.next().ok_or(Error::NotEnoughWords)?.try_into().ok().ok_or(Error::IdZero) 211 | }; 212 | self.inst.result_type_id = def.has_result_type_id.then(&mut id).transpose()?; 213 | self.inst.result_id = def.has_result_id.then(&mut id).transpose()?; 214 | } 215 | 216 | if let Some(type_id) = self.inst.result_type_id { 217 | if !self.known_ids.contains_key(&type_id) { 218 | // FIXME(eddyb) also check that the ID is a valid type. 219 | return Err(Error::UnknownResultTypeId(type_id)); 220 | } 221 | } 222 | 223 | for (mode, kind) in def.all_operands() { 224 | if mode == spec::OperandMode::Optional && self.is_exhausted() { 225 | break; 226 | } 227 | self.operand(kind)?; 228 | } 229 | 230 | // The instruction must consume its entire word count. 231 | if !self.is_exhausted() { 232 | return Err(Error::TooManyWords); 233 | } 234 | 235 | Ok(self.inst) 236 | } 237 | } 238 | 239 | pub struct ModuleParser { 240 | /// Copy of the header words (for convenience). 241 | // FIXME(eddyb) add a `spec::Header` or `spv::Header` struct with named fields. 242 | pub header: [u32; spec::HEADER_LEN], 243 | 244 | /// The entire module's bytes, representing "native endian" SPIR-V words. 245 | // FIXME(eddyb) could this be allocated as `Vec` in the first place? 246 | word_bytes: Vec, 247 | 248 | /// Next (instructions') word position in the module. 249 | next_word: usize, 250 | 251 | /// IDs defined so far in the module. 252 | known_ids: FxHashMap, 253 | } 254 | 255 | // FIXME(eddyb) stop abusing `io::Error` for error reporting. 256 | fn invalid(reason: &str) -> io::Error { 257 | io::Error::new(io::ErrorKind::InvalidData, format!("malformed SPIR-V ({reason})")) 258 | } 259 | 260 | impl ModuleParser { 261 | pub fn read_from_spv_file(path: impl AsRef) -> io::Result { 262 | Self::read_from_spv_bytes(fs::read(path)?) 263 | } 264 | 265 | // FIXME(eddyb) also add `from_spv_words`. 266 | pub fn read_from_spv_bytes(spv_bytes: Vec) -> io::Result { 267 | let spv_spec = spec::Spec::get(); 268 | 269 | if spv_bytes.len() % 4 != 0 { 270 | return Err(invalid("not a multiple of 4 bytes")); 271 | } 272 | // May need to mutate the bytes (to normalize endianness) later below. 273 | let mut spv_bytes = spv_bytes; 274 | let spv_words = bytemuck::cast_slice_mut::(&mut spv_bytes); 275 | 276 | if spv_words.len() < spec::HEADER_LEN { 277 | return Err(invalid("truncated header")); 278 | } 279 | 280 | // Check the magic, and swap endianness of all words if we have to. 281 | { 282 | let magic = spv_words[0]; 283 | if magic == spv_spec.magic { 284 | // Nothing to do, all words already match native endianness. 285 | } else if magic.swap_bytes() == spv_spec.magic { 286 | for word in &mut spv_words[..] { 287 | *word = word.swap_bytes(); 288 | } 289 | } else { 290 | return Err(invalid("incorrect magic number")); 291 | } 292 | } 293 | 294 | Ok(Self { 295 | header: spv_words[..spec::HEADER_LEN].try_into().unwrap(), 296 | word_bytes: spv_bytes, 297 | next_word: spec::HEADER_LEN, 298 | 299 | known_ids: FxHashMap::default(), 300 | }) 301 | } 302 | } 303 | 304 | impl Iterator for ModuleParser { 305 | type Item = io::Result; 306 | fn next(&mut self) -> Option { 307 | let spv_spec = spec::Spec::get(); 308 | let wk = &spv_spec.well_known; 309 | 310 | let words = &bytemuck::cast_slice::(&self.word_bytes)[self.next_word..]; 311 | let &opcode = words.first()?; 312 | 313 | let (inst_len, opcode) = ((opcode >> 16) as usize, opcode as u16); 314 | 315 | let (opcode, inst_name, def) = match spec::Opcode::try_from_u16_with_name_and_def(opcode) { 316 | Some(opcode_name_and_def) => opcode_name_and_def, 317 | None => return Some(Err(invalid(&format!("unsupported opcode {opcode}")))), 318 | }; 319 | 320 | let invalid = |msg: &str| invalid(&format!("in {inst_name}: {msg}")); 321 | 322 | if words.len() < inst_len { 323 | return Some(Err(invalid("truncated instruction"))); 324 | } 325 | 326 | let parser = InstParser { 327 | known_ids: &self.known_ids, 328 | words: words[1..inst_len].iter().copied(), 329 | inst: spv::InstWithIds { 330 | without_ids: opcode.into(), 331 | result_type_id: None, 332 | result_id: None, 333 | ids: SmallVec::new(), 334 | }, 335 | }; 336 | 337 | let inst = match parser.inst(def) { 338 | Ok(inst) => inst, 339 | Err(e) => return Some(Err(invalid(&e.message()))), 340 | }; 341 | 342 | // HACK(eddyb) `Option::map` allows using `?` for `Result` in the closure. 343 | let maybe_known_id_result = inst.result_id.map(|id| { 344 | let known_id_def = if opcode == wk.OpTypeInt { 345 | KnownIdDef::TypeInt(match inst.imms[0] { 346 | spv::Imm::Short(kind, n) => { 347 | assert_eq!(kind, wk.LiteralInteger); 348 | n.try_into().ok().ok_or_else(|| invalid("Width cannot be 0"))? 349 | } 350 | _ => unreachable!(), 351 | }) 352 | } else if opcode == wk.OpTypeFloat { 353 | KnownIdDef::TypeFloat(match inst.imms[0] { 354 | spv::Imm::Short(kind, n) => { 355 | assert_eq!(kind, wk.LiteralInteger); 356 | n.try_into().ok().ok_or_else(|| invalid("Width cannot be 0"))? 357 | } 358 | _ => unreachable!(), 359 | }) 360 | } else { 361 | KnownIdDef::Uncategorized { opcode, result_type_id: inst.result_type_id } 362 | }; 363 | 364 | let old = self.known_ids.insert(id, known_id_def); 365 | if old.is_some() { 366 | return Err(invalid(&format!("ID %{id} is a result of multiple instructions"))); 367 | } 368 | 369 | Ok(()) 370 | }); 371 | if let Some(Err(e)) = maybe_known_id_result { 372 | return Some(Err(e)); 373 | } 374 | 375 | self.next_word += inst_len; 376 | 377 | Some(Ok(inst)) 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /src/visit.rs: -------------------------------------------------------------------------------- 1 | //! Immutable IR traversal. 2 | 3 | use crate::func_at::FuncAt; 4 | use crate::qptr::{self, QPtrAttr, QPtrMemUsage, QPtrMemUsageKind, QPtrOp, QPtrUsage}; 5 | use crate::{ 6 | cfg, spv, AddrSpace, Attr, AttrSet, AttrSetDef, Const, ConstDef, ConstKind, ControlNode, 7 | ControlNodeDef, ControlNodeKind, ControlNodeOutputDecl, ControlRegion, ControlRegionDef, 8 | ControlRegionInputDecl, DataInstDef, DataInstForm, DataInstFormDef, DataInstKind, DeclDef, 9 | DiagMsgPart, EntityListIter, ExportKey, Exportee, Func, FuncDecl, FuncDefBody, FuncParam, 10 | GlobalVar, GlobalVarDecl, GlobalVarDefBody, Import, Module, ModuleDebugInfo, ModuleDialect, 11 | SelectionKind, Type, TypeDef, TypeKind, TypeOrConst, Value, 12 | }; 13 | 14 | // FIXME(eddyb) `Sized` bound shouldn't be needed but removing it requires 15 | // writing `impl Visitor<'a> + ?Sized` in `fn inner_visit_with` signatures. 16 | pub trait Visitor<'a>: Sized { 17 | // Context-interned leaves (no default provided). 18 | // FIXME(eddyb) treat these separately somehow and allow e.g. automatic deep 19 | // visiting (with a set to avoid repeat visits) if a `Rc` is provided. 20 | fn visit_attr_set_use(&mut self, attrs: AttrSet); 21 | fn visit_type_use(&mut self, ty: Type); 22 | fn visit_const_use(&mut self, ct: Const); 23 | fn visit_data_inst_form_use(&mut self, data_inst_form: DataInstForm); 24 | 25 | // Module-stored entity leaves (no default provided). 26 | fn visit_global_var_use(&mut self, gv: GlobalVar); 27 | fn visit_func_use(&mut self, func: Func); 28 | 29 | // Leaves (noop default behavior). 30 | fn visit_spv_dialect(&mut self, _dialect: &spv::Dialect) {} 31 | fn visit_spv_module_debug_info(&mut self, _debug_info: &spv::ModuleDebugInfo) {} 32 | fn visit_import(&mut self, _import: &Import) {} 33 | 34 | // Non-leaves (defaulting to calling `.inner_visit_with(self)`). 35 | fn visit_module(&mut self, module: &'a Module) { 36 | module.inner_visit_with(self); 37 | } 38 | fn visit_module_dialect(&mut self, dialect: &'a ModuleDialect) { 39 | dialect.inner_visit_with(self); 40 | } 41 | fn visit_module_debug_info(&mut self, debug_info: &'a ModuleDebugInfo) { 42 | debug_info.inner_visit_with(self); 43 | } 44 | fn visit_attr_set_def(&mut self, attrs_def: &'a AttrSetDef) { 45 | attrs_def.inner_visit_with(self); 46 | } 47 | fn visit_attr(&mut self, attr: &'a Attr) { 48 | attr.inner_visit_with(self); 49 | } 50 | fn visit_type_def(&mut self, ty_def: &'a TypeDef) { 51 | ty_def.inner_visit_with(self); 52 | } 53 | fn visit_const_def(&mut self, ct_def: &'a ConstDef) { 54 | ct_def.inner_visit_with(self); 55 | } 56 | fn visit_global_var_decl(&mut self, gv_decl: &'a GlobalVarDecl) { 57 | gv_decl.inner_visit_with(self); 58 | } 59 | fn visit_func_decl(&mut self, func_decl: &'a FuncDecl) { 60 | func_decl.inner_visit_with(self); 61 | } 62 | fn visit_control_region_def(&mut self, func_at_control_region: FuncAt<'a, ControlRegion>) { 63 | func_at_control_region.inner_visit_with(self); 64 | } 65 | fn visit_control_node_def(&mut self, func_at_control_node: FuncAt<'a, ControlNode>) { 66 | func_at_control_node.inner_visit_with(self); 67 | } 68 | fn visit_data_inst_def(&mut self, data_inst_def: &'a DataInstDef) { 69 | data_inst_def.inner_visit_with(self); 70 | } 71 | fn visit_data_inst_form_def(&mut self, data_inst_form_def: &'a DataInstFormDef) { 72 | data_inst_form_def.inner_visit_with(self); 73 | } 74 | fn visit_value_use(&mut self, v: &'a Value) { 75 | v.inner_visit_with(self); 76 | } 77 | } 78 | 79 | /// Trait implemented on "visitable" types (shallowly visitable, at least). 80 | /// 81 | /// That is, an `impl Visit for X` will call the relevant [`Visitor`] method for 82 | /// `X`, typically named `Visitor::visit_X` or `Visitor::visit_X_use`. 83 | // 84 | // FIXME(eddyb) use this more (e.g. in implementing `InnerVisit`). 85 | pub trait Visit { 86 | fn visit_with<'a>(&'a self, visitor: &mut impl Visitor<'a>); 87 | } 88 | 89 | macro_rules! impl_visit { 90 | ( 91 | by_val { $($by_val_method:ident($by_val_ty:ty)),* $(,)? } 92 | by_ref { $($by_ref_method:ident($by_ref_ty:ty)),* $(,)? } 93 | forward_to_inner_visit { $($forward_to_inner_visit_ty:ty),* $(,)? } 94 | ) => { 95 | $(impl Visit for $by_val_ty { 96 | fn visit_with<'a>(&'a self, visitor: &mut impl Visitor<'a>) { 97 | visitor.$by_val_method(*self); 98 | } 99 | })* 100 | $(impl Visit for $by_ref_ty { 101 | fn visit_with<'a>(&'a self, visitor: &mut impl Visitor<'a>) { 102 | visitor.$by_ref_method(self); 103 | } 104 | })* 105 | $(impl Visit for $forward_to_inner_visit_ty { 106 | fn visit_with<'a>(&'a self, visitor: &mut impl Visitor<'a>) { 107 | self.inner_visit_with(visitor); 108 | } 109 | })* 110 | }; 111 | } 112 | 113 | impl_visit! { 114 | by_val { 115 | visit_attr_set_use(AttrSet), 116 | visit_type_use(Type), 117 | visit_const_use(Const), 118 | visit_global_var_use(GlobalVar), 119 | visit_func_use(Func), 120 | } 121 | by_ref { 122 | visit_spv_dialect(spv::Dialect), 123 | visit_spv_module_debug_info(spv::ModuleDebugInfo), 124 | visit_import(Import), 125 | visit_module(Module), 126 | visit_module_dialect(ModuleDialect), 127 | visit_module_debug_info(ModuleDebugInfo), 128 | visit_attr_set_def(AttrSetDef), 129 | visit_attr(Attr), 130 | visit_type_def(TypeDef), 131 | visit_const_def(ConstDef), 132 | visit_global_var_decl(GlobalVarDecl), 133 | visit_func_decl(FuncDecl), 134 | visit_data_inst_def(DataInstDef), 135 | visit_value_use(Value), 136 | } 137 | forward_to_inner_visit { 138 | // NOTE(eddyb) the interpolated parts of `Attr::Diagnostics` aren't visited 139 | // by default (as they're "inert data"), this is only for `print`'s usage. 140 | Vec, 141 | } 142 | } 143 | 144 | /// Trait implemented on "deeply visitable" types, to further "explore" a type 145 | /// by visiting its "interior" (i.e. variants and/or fields). 146 | /// 147 | /// That is, an `impl InnerVisit for X` will call the relevant [`Visitor`] method 148 | /// for each `X` field, effectively performing a single level of a deep visit. 149 | /// Also, if `Visitor::visit_X` exists for a given `X`, its default should be to 150 | /// call `X::inner_visit_with` (i.e. so that visiting is mostly-deep by default). 151 | pub trait InnerVisit { 152 | // FIXME(eddyb) the naming here isn't great, can it be improved? 153 | fn inner_visit_with<'a>(&'a self, visitor: &mut impl Visitor<'a>); 154 | } 155 | 156 | /// Dynamic dispatch version of [`InnerVisit`]. 157 | /// 158 | /// `dyn DynInnerVisit<'a, V>` is possible, unlike `dyn InnerVisit`, because of 159 | /// the `trait`-level type parameter `V`, which replaces the method parameter. 160 | pub trait DynInnerVisit<'a, V> { 161 | fn dyn_inner_visit_with(&'a self, visitor: &mut V); 162 | } 163 | 164 | impl<'a, T: InnerVisit, V: Visitor<'a>> DynInnerVisit<'a, V> for T { 165 | fn dyn_inner_visit_with(&'a self, visitor: &mut V) { 166 | self.inner_visit_with(visitor); 167 | } 168 | } 169 | 170 | // FIXME(eddyb) should the impls be here, or next to definitions? (maybe derived?) 171 | impl InnerVisit for Module { 172 | fn inner_visit_with<'a>(&'a self, visitor: &mut impl Visitor<'a>) { 173 | // FIXME(eddyb) this can't be exhaustive because of the private `cx` field. 174 | let Self { dialect, debug_info, global_vars: _, funcs: _, exports, .. } = self; 175 | 176 | visitor.visit_module_dialect(dialect); 177 | visitor.visit_module_debug_info(debug_info); 178 | for (export_key, exportee) in exports { 179 | export_key.inner_visit_with(visitor); 180 | exportee.inner_visit_with(visitor); 181 | } 182 | } 183 | } 184 | 185 | impl InnerVisit for ModuleDialect { 186 | fn inner_visit_with<'a>(&'a self, visitor: &mut impl Visitor<'a>) { 187 | match self { 188 | Self::Spv(dialect) => visitor.visit_spv_dialect(dialect), 189 | } 190 | } 191 | } 192 | 193 | impl InnerVisit for ModuleDebugInfo { 194 | fn inner_visit_with<'a>(&'a self, visitor: &mut impl Visitor<'a>) { 195 | match self { 196 | Self::Spv(debug_info) => { 197 | visitor.visit_spv_module_debug_info(debug_info); 198 | } 199 | } 200 | } 201 | } 202 | 203 | impl InnerVisit for ExportKey { 204 | fn inner_visit_with<'a>(&'a self, visitor: &mut impl Visitor<'a>) { 205 | match self { 206 | Self::LinkName(_) => {} 207 | 208 | Self::SpvEntryPoint { imms: _, interface_global_vars } => { 209 | for &gv in interface_global_vars { 210 | visitor.visit_global_var_use(gv); 211 | } 212 | } 213 | } 214 | } 215 | } 216 | 217 | impl InnerVisit for Exportee { 218 | fn inner_visit_with<'a>(&'a self, visitor: &mut impl Visitor<'a>) { 219 | match *self { 220 | Self::GlobalVar(gv) => visitor.visit_global_var_use(gv), 221 | Self::Func(func) => visitor.visit_func_use(func), 222 | } 223 | } 224 | } 225 | 226 | impl InnerVisit for AttrSetDef { 227 | fn inner_visit_with<'a>(&'a self, visitor: &mut impl Visitor<'a>) { 228 | let Self { attrs } = self; 229 | 230 | for attr in attrs { 231 | visitor.visit_attr(attr); 232 | } 233 | } 234 | } 235 | 236 | impl InnerVisit for Attr { 237 | fn inner_visit_with<'a>(&'a self, visitor: &mut impl Visitor<'a>) { 238 | match self { 239 | Attr::Diagnostics(_) 240 | | Attr::SpvAnnotation(_) 241 | | Attr::SpvDebugLine { .. } 242 | | Attr::SpvBitflagsOperand(_) => {} 243 | 244 | Attr::QPtr(attr) => match attr { 245 | QPtrAttr::ToSpvPtrInput { input_idx: _, pointee } 246 | | QPtrAttr::FromSpvPtrOutput { addr_space: _, pointee } => { 247 | visitor.visit_type_use(pointee.0); 248 | } 249 | 250 | QPtrAttr::Usage(usage) => usage.0.inner_visit_with(visitor), 251 | }, 252 | } 253 | } 254 | } 255 | 256 | // NOTE(eddyb) the interpolated parts of `Attr::Diagnostics` aren't visited 257 | // by default (as they're "inert data"), this is only for `print`'s usage. 258 | impl InnerVisit for Vec { 259 | fn inner_visit_with<'a>(&'a self, visitor: &mut impl Visitor<'a>) { 260 | for part in self { 261 | match part { 262 | DiagMsgPart::Plain(_) => {} 263 | &DiagMsgPart::Attrs(attrs) => visitor.visit_attr_set_use(attrs), 264 | &DiagMsgPart::Type(ty) => visitor.visit_type_use(ty), 265 | &DiagMsgPart::Const(ct) => visitor.visit_const_use(ct), 266 | DiagMsgPart::QPtrUsage(usage) => usage.inner_visit_with(visitor), 267 | } 268 | } 269 | } 270 | } 271 | 272 | impl InnerVisit for QPtrUsage { 273 | fn inner_visit_with<'a>(&'a self, visitor: &mut impl Visitor<'a>) { 274 | match self { 275 | &QPtrUsage::Handles(qptr::shapes::Handle::Opaque(ty)) => { 276 | visitor.visit_type_use(ty); 277 | } 278 | QPtrUsage::Handles(qptr::shapes::Handle::Buffer(_, data_usage)) => { 279 | data_usage.inner_visit_with(visitor); 280 | } 281 | QPtrUsage::Memory(usage) => usage.inner_visit_with(visitor), 282 | } 283 | } 284 | } 285 | 286 | impl InnerVisit for QPtrMemUsage { 287 | fn inner_visit_with<'a>(&'a self, visitor: &mut impl Visitor<'a>) { 288 | let Self { max_size: _, kind } = self; 289 | kind.inner_visit_with(visitor); 290 | } 291 | } 292 | 293 | impl InnerVisit for QPtrMemUsageKind { 294 | fn inner_visit_with<'a>(&'a self, visitor: &mut impl Visitor<'a>) { 295 | match self { 296 | Self::Unused => {} 297 | &Self::StrictlyTyped(ty) | &Self::DirectAccess(ty) => { 298 | visitor.visit_type_use(ty); 299 | } 300 | Self::OffsetBase(entries) => { 301 | for sub_usage in entries.values() { 302 | sub_usage.inner_visit_with(visitor); 303 | } 304 | } 305 | Self::DynOffsetBase { element, stride: _ } => { 306 | element.inner_visit_with(visitor); 307 | } 308 | } 309 | } 310 | } 311 | 312 | impl InnerVisit for TypeDef { 313 | fn inner_visit_with<'a>(&'a self, visitor: &mut impl Visitor<'a>) { 314 | let Self { attrs, kind } = self; 315 | 316 | visitor.visit_attr_set_use(*attrs); 317 | match kind { 318 | TypeKind::QPtr | TypeKind::SpvStringLiteralForExtInst => {} 319 | 320 | TypeKind::SpvInst { spv_inst: _, type_and_const_inputs } => { 321 | for &ty_or_ct in type_and_const_inputs { 322 | match ty_or_ct { 323 | TypeOrConst::Type(ty) => visitor.visit_type_use(ty), 324 | TypeOrConst::Const(ct) => visitor.visit_const_use(ct), 325 | } 326 | } 327 | } 328 | } 329 | } 330 | } 331 | 332 | impl InnerVisit for ConstDef { 333 | fn inner_visit_with<'a>(&'a self, visitor: &mut impl Visitor<'a>) { 334 | let Self { attrs, ty, kind } = self; 335 | 336 | visitor.visit_attr_set_use(*attrs); 337 | visitor.visit_type_use(*ty); 338 | match kind { 339 | &ConstKind::PtrToGlobalVar(gv) => visitor.visit_global_var_use(gv), 340 | ConstKind::SpvInst { spv_inst_and_const_inputs } => { 341 | let (_spv_inst, const_inputs) = &**spv_inst_and_const_inputs; 342 | for &ct in const_inputs { 343 | visitor.visit_const_use(ct); 344 | } 345 | } 346 | ConstKind::SpvStringLiteralForExtInst(_) => {} 347 | } 348 | } 349 | } 350 | 351 | impl InnerVisit for DeclDef { 352 | fn inner_visit_with<'a>(&'a self, visitor: &mut impl Visitor<'a>) { 353 | match self { 354 | Self::Imported(import) => visitor.visit_import(import), 355 | Self::Present(def) => def.inner_visit_with(visitor), 356 | } 357 | } 358 | } 359 | 360 | impl InnerVisit for GlobalVarDecl { 361 | fn inner_visit_with<'a>(&'a self, visitor: &mut impl Visitor<'a>) { 362 | let Self { attrs, type_of_ptr_to, shape, addr_space, def } = self; 363 | 364 | visitor.visit_attr_set_use(*attrs); 365 | visitor.visit_type_use(*type_of_ptr_to); 366 | if let Some(shape) = shape { 367 | match shape { 368 | qptr::shapes::GlobalVarShape::TypedInterface(ty) => visitor.visit_type_use(*ty), 369 | qptr::shapes::GlobalVarShape::Handles { .. } 370 | | qptr::shapes::GlobalVarShape::UntypedData(_) => {} 371 | } 372 | } 373 | match addr_space { 374 | AddrSpace::Handles | AddrSpace::SpvStorageClass(_) => {} 375 | } 376 | def.inner_visit_with(visitor); 377 | } 378 | } 379 | 380 | impl InnerVisit for GlobalVarDefBody { 381 | fn inner_visit_with<'a>(&'a self, visitor: &mut impl Visitor<'a>) { 382 | let Self { initializer } = self; 383 | 384 | if let Some(initializer) = *initializer { 385 | visitor.visit_const_use(initializer); 386 | } 387 | } 388 | } 389 | 390 | impl InnerVisit for FuncDecl { 391 | fn inner_visit_with<'a>(&'a self, visitor: &mut impl Visitor<'a>) { 392 | let Self { attrs, ret_type, params, def } = self; 393 | 394 | visitor.visit_attr_set_use(*attrs); 395 | visitor.visit_type_use(*ret_type); 396 | for param in params { 397 | param.inner_visit_with(visitor); 398 | } 399 | def.inner_visit_with(visitor); 400 | } 401 | } 402 | 403 | impl InnerVisit for FuncParam { 404 | fn inner_visit_with<'a>(&'a self, visitor: &mut impl Visitor<'a>) { 405 | let Self { attrs, ty } = *self; 406 | 407 | visitor.visit_attr_set_use(attrs); 408 | visitor.visit_type_use(ty); 409 | } 410 | } 411 | 412 | impl InnerVisit for FuncDefBody { 413 | fn inner_visit_with<'a>(&'a self, visitor: &mut impl Visitor<'a>) { 414 | match &self.unstructured_cfg { 415 | None => visitor.visit_control_region_def(self.at_body()), 416 | Some(cfg) => { 417 | for region in cfg.rev_post_order(self) { 418 | visitor.visit_control_region_def(self.at(region)); 419 | 420 | if let Some(control_inst) = cfg.control_inst_on_exit_from.get(region) { 421 | control_inst.inner_visit_with(visitor); 422 | } 423 | } 424 | } 425 | } 426 | } 427 | } 428 | 429 | // FIXME(eddyb) this can't implement `InnerVisit` because of the `&'a self` 430 | // requirement, whereas this has `'a` in `self: FuncAt<'a, ControlRegion>`. 431 | impl<'a> FuncAt<'a, ControlRegion> { 432 | pub fn inner_visit_with(self, visitor: &mut impl Visitor<'a>) { 433 | let ControlRegionDef { inputs, children, outputs } = self.def(); 434 | 435 | for input in inputs { 436 | input.inner_visit_with(visitor); 437 | } 438 | self.at(*children).into_iter().inner_visit_with(visitor); 439 | for v in outputs { 440 | visitor.visit_value_use(v); 441 | } 442 | } 443 | } 444 | 445 | impl InnerVisit for ControlRegionInputDecl { 446 | fn inner_visit_with<'a>(&'a self, visitor: &mut impl Visitor<'a>) { 447 | let Self { attrs, ty } = *self; 448 | 449 | visitor.visit_attr_set_use(attrs); 450 | visitor.visit_type_use(ty); 451 | } 452 | } 453 | 454 | // FIXME(eddyb) this can't implement `InnerVisit` because of the `&'a self` 455 | // requirement, whereas this has `'a` in `self: FuncAt<'a, ...>`. 456 | impl<'a> FuncAt<'a, EntityListIter> { 457 | pub fn inner_visit_with(self, visitor: &mut impl Visitor<'a>) { 458 | for func_at_control_node in self { 459 | visitor.visit_control_node_def(func_at_control_node); 460 | } 461 | } 462 | } 463 | 464 | // FIXME(eddyb) this can't implement `InnerVisit` because of the `&'a self` 465 | // requirement, whereas this has `'a` in `self: FuncAt<'a, ControlNode>`. 466 | impl<'a> FuncAt<'a, ControlNode> { 467 | pub fn inner_visit_with(self, visitor: &mut impl Visitor<'a>) { 468 | let ControlNodeDef { kind, outputs } = self.def(); 469 | 470 | match kind { 471 | ControlNodeKind::Block { insts } => { 472 | for func_at_inst in self.at(*insts) { 473 | visitor.visit_data_inst_def(func_at_inst.def()); 474 | } 475 | } 476 | ControlNodeKind::Select { 477 | kind: SelectionKind::BoolCond | SelectionKind::SpvInst(_), 478 | scrutinee, 479 | cases, 480 | } => { 481 | visitor.visit_value_use(scrutinee); 482 | for &case in cases { 483 | visitor.visit_control_region_def(self.at(case)); 484 | } 485 | } 486 | ControlNodeKind::Loop { initial_inputs, body, repeat_condition } => { 487 | for v in initial_inputs { 488 | visitor.visit_value_use(v); 489 | } 490 | visitor.visit_control_region_def(self.at(*body)); 491 | visitor.visit_value_use(repeat_condition); 492 | } 493 | } 494 | for output in outputs { 495 | output.inner_visit_with(visitor); 496 | } 497 | } 498 | } 499 | 500 | impl InnerVisit for ControlNodeOutputDecl { 501 | fn inner_visit_with<'a>(&'a self, visitor: &mut impl Visitor<'a>) { 502 | let Self { attrs, ty } = *self; 503 | 504 | visitor.visit_attr_set_use(attrs); 505 | visitor.visit_type_use(ty); 506 | } 507 | } 508 | 509 | impl InnerVisit for DataInstDef { 510 | fn inner_visit_with<'a>(&'a self, visitor: &mut impl Visitor<'a>) { 511 | let Self { attrs, form, inputs } = self; 512 | 513 | visitor.visit_attr_set_use(*attrs); 514 | visitor.visit_data_inst_form_use(*form); 515 | for v in inputs { 516 | visitor.visit_value_use(v); 517 | } 518 | } 519 | } 520 | 521 | impl InnerVisit for DataInstFormDef { 522 | fn inner_visit_with<'a>(&'a self, visitor: &mut impl Visitor<'a>) { 523 | let Self { kind, output_type } = self; 524 | 525 | match kind { 526 | &DataInstKind::FuncCall(func) => visitor.visit_func_use(func), 527 | DataInstKind::QPtr(op) => match *op { 528 | QPtrOp::FuncLocalVar(_) 529 | | QPtrOp::HandleArrayIndex 530 | | QPtrOp::BufferData 531 | | QPtrOp::BufferDynLen { .. } 532 | | QPtrOp::Offset(_) 533 | | QPtrOp::DynOffset { .. } 534 | | QPtrOp::Load 535 | | QPtrOp::Store => {} 536 | }, 537 | DataInstKind::SpvInst(_) | DataInstKind::SpvExtInst { .. } => {} 538 | } 539 | if let Some(ty) = *output_type { 540 | visitor.visit_type_use(ty); 541 | } 542 | } 543 | } 544 | 545 | impl InnerVisit for cfg::ControlInst { 546 | fn inner_visit_with<'a>(&'a self, visitor: &mut impl Visitor<'a>) { 547 | let Self { attrs, kind, inputs, targets: _, target_inputs } = self; 548 | 549 | visitor.visit_attr_set_use(*attrs); 550 | match kind { 551 | cfg::ControlInstKind::Unreachable 552 | | cfg::ControlInstKind::Return 553 | | cfg::ControlInstKind::ExitInvocation(cfg::ExitInvocationKind::SpvInst(_)) 554 | | cfg::ControlInstKind::Branch 555 | | cfg::ControlInstKind::SelectBranch( 556 | SelectionKind::BoolCond | SelectionKind::SpvInst(_), 557 | ) => {} 558 | } 559 | for v in inputs { 560 | visitor.visit_value_use(v); 561 | } 562 | for inputs in target_inputs.values() { 563 | for v in inputs { 564 | visitor.visit_value_use(v); 565 | } 566 | } 567 | } 568 | } 569 | 570 | impl InnerVisit for Value { 571 | fn inner_visit_with<'a>(&'a self, visitor: &mut impl Visitor<'a>) { 572 | match *self { 573 | Self::Const(ct) => visitor.visit_const_use(ct), 574 | Self::ControlRegionInput { region: _, input_idx: _ } 575 | | Self::ControlNodeOutput { control_node: _, output_idx: _ } 576 | | Self::DataInstOutput(_) => {} 577 | } 578 | } 579 | } 580 | -------------------------------------------------------------------------------- /src/print/multiversion.rs: -------------------------------------------------------------------------------- 1 | //! Multi-version pretty-printing support (e.g. for comparing the IR between passes). 2 | 3 | use crate::print::pretty::{self, TextOp}; 4 | use crate::FxIndexMap; 5 | use internal_iterator::{ 6 | FromInternalIterator, InternalIterator, IntoInternalIterator, IteratorExt, 7 | }; 8 | use itertools::{Either, Itertools}; 9 | use smallvec::SmallVec; 10 | use std::fmt::Write; 11 | use std::{fmt, iter, mem}; 12 | 13 | #[allow(rustdoc::private_intra_doc_links)] 14 | /// Wrapper for handling the difference between single-version and multi-version 15 | /// output, which aren't expressible in [`pretty::Fragment`]. 16 | // 17 | // FIXME(eddyb) introduce a `pretty::Node` variant capable of handling this, 18 | // but that's complicated wrt non-HTML output, if they're to also be 2D tables. 19 | pub enum Versions { 20 | Single(PF), 21 | Multiple { 22 | // FIXME(eddyb) avoid allocating this if possible. 23 | version_names: Vec, 24 | 25 | /// Each row consists of *deduplicated* (or "run-length encoded") 26 | /// versions, with "repeat count"s larger than `1` indicating that 27 | /// multiple versions (columns) have the exact same content. 28 | /// 29 | /// For HTML output, "repeat count"s map to `colspan` attributes. 30 | // 31 | // FIXME(eddyb) remove the "repeat count" mechanism. 32 | per_row_versions_with_repeat_count: Vec>, 33 | }, 34 | } 35 | 36 | impl fmt::Display for Versions { 37 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 38 | match self { 39 | Self::Single(fragment) => fragment.fmt(f), 40 | Self::Multiple { version_names, per_row_versions_with_repeat_count } => { 41 | let mut first = true; 42 | 43 | // HACK(eddyb) this is not the nicest output, but multi-version 44 | // is intended for HTML input primarily anyway. 45 | for versions_with_repeat_count in per_row_versions_with_repeat_count { 46 | if !first { 47 | writeln!(f)?; 48 | } 49 | first = false; 50 | 51 | let mut next_version_idx = 0; 52 | let mut any_headings = false; 53 | for (fragment, repeat_count) in versions_with_repeat_count { 54 | // No headings for anything uniform across versions. 55 | if (next_version_idx, *repeat_count) != (0, version_names.len()) { 56 | any_headings = true; 57 | 58 | if next_version_idx == 0 { 59 | write!(f, "//#IF ")?; 60 | } else { 61 | write!(f, "//#ELSEIF ")?; 62 | } 63 | let mut first_name = true; 64 | for name in &version_names[next_version_idx..][..*repeat_count] { 65 | if !first_name { 66 | write!(f, " | ")?; 67 | } 68 | first_name = false; 69 | 70 | write!(f, "`{name}`")?; 71 | } 72 | writeln!(f)?; 73 | } 74 | 75 | writeln!(f, "{fragment}")?; 76 | 77 | next_version_idx += repeat_count; 78 | } 79 | if any_headings { 80 | writeln!(f, "//#ENDIF")?; 81 | } 82 | } 83 | 84 | Ok(()) 85 | } 86 | } 87 | } 88 | } 89 | 90 | impl Versions { 91 | // FIXME(eddyb) provide a non-allocating version. 92 | pub fn render_to_html(&self) -> pretty::HtmlSnippet { 93 | match self { 94 | Self::Single(fragment) => fragment.render_to_html(), 95 | Self::Multiple { version_names, per_row_versions_with_repeat_count } => { 96 | // HACK(eddyb) using an UUID as a class name in lieu of "scoped 133 | " 134 | .replace("SCOPE", &format!("table.{TABLE_CLASS_NAME}")) 135 | .replace("MAX_LINE_WIDTH", &super::MAX_LINE_WIDTH.to_string()), 136 | ); 137 | 138 | let headings = { 139 | let mut h = "".to_string(); 140 | for name in version_names { 141 | write!(h, "{name}").unwrap(); 142 | } 143 | h + "\n" 144 | }; 145 | 146 | html.body = format!("\n"); 147 | let mut last_was_uniform = true; 148 | for versions_with_repeat_count in per_row_versions_with_repeat_count { 149 | // FIXME(eddyb) remove the "repeat count" mechanism. 150 | let is_uniform = match versions_with_repeat_count[..] { 151 | [(_, repeat_count)] => repeat_count == version_names.len(), 152 | _ => false, 153 | }; 154 | 155 | if last_was_uniform && is_uniform { 156 | // Headings unnecessary, they would be between uniform 157 | // rows (or at the very start, before an uniform row). 158 | } else { 159 | // Repeat the headings often, where necessary. 160 | html.body += &headings; 161 | } 162 | last_was_uniform = is_uniform; 163 | 164 | // Attempt to align as many anchors as possible between the 165 | // columns, to improve legibility (see also `AnchorAligner`). 166 | let mut anchor_aligner = AnchorAligner::default(); 167 | for (fragment, _) in versions_with_repeat_count { 168 | anchor_aligner 169 | .add_column_and_align_anchors(fragment.render_to_text_ops().collect()); 170 | } 171 | 172 | html.body += "\n"; 173 | if is_uniform { 174 | // FIXME(eddyb) avoid duplication with the non-uniform case. 175 | let pretty::HtmlSnippet { 176 | head_deduplicatable_elements: fragment_head, 177 | body: fragment_body, 178 | } = anchor_aligner 179 | .merged_columns() 180 | .next() 181 | .unwrap() 182 | .lines() 183 | .intersperse(&[TextOp::Text("\n")]) 184 | .flatten() 185 | .copied() 186 | .into_internal() 187 | .collect(); 188 | 189 | html.head_deduplicatable_elements.extend(fragment_head); 190 | 191 | writeln!(html.body, "\n"; 194 | } else { 195 | let mut merged_columns = versions_with_repeat_count 196 | .iter() 197 | .zip(anchor_aligner.merged_columns()) 198 | .flat_map(|(&(_, repeat_count), column)| { 199 | iter::repeat(column).take(repeat_count) 200 | }) 201 | .peekable(); 202 | 203 | let mut prev_column = None; 204 | while let Some(column) = merged_columns.next() { 205 | let prev_column = prev_column.replace(column); 206 | let next_column = merged_columns.peek().copied(); 207 | 208 | let unchanged_line_style = pretty::Styles { 209 | desaturate_and_dim_for_unchanged_multiversion_line: true, 210 | ..Default::default() 211 | }; 212 | 213 | // NOTE(eddyb) infinite (but limited by `zip` below), 214 | // and `Some([])`/`None` distinguishes empty/missing. 215 | let prev_lines = prev_column 216 | .iter() 217 | .flat_map(|prev| prev.lines().map(Some)) 218 | .chain(iter::repeat(prev_column.map(|_| &[][..]))); 219 | let next_lines = next_column 220 | .iter() 221 | .flat_map(|next| next.lines().map(Some)) 222 | .chain(iter::repeat(next_column.map(|_| &[][..]))); 223 | 224 | let lines = column.lines().zip(prev_lines).zip(next_lines).map( 225 | |((line, prev_line), next_line)| { 226 | // FIXME(eddyb) apply a `class` instead of an inline `style`, 227 | // and allow `:hover` to disable the desaturation/dimming. 228 | // FIXME(eddyb) maybe indicate when lines 229 | // were removed (red "hashed" background?). 230 | let diff = |other: Option<_>| { 231 | // Ignore indendation-only changes. 232 | fn strip_indents<'a, 'b>( 233 | mut line: &'b [TextOp<'a>], 234 | ) -> &'b [TextOp<'a>] 235 | { 236 | // HACK(eddyb) also ignore helper anchors, 237 | // which can go before indents. 238 | while let [TextOp::Text(pretty::INDENT), rest @ ..] 239 | | [ 240 | TextOp::PushAnchor { .. }, 241 | TextOp::PopAnchor { .. }, 242 | rest @ .., 243 | ] = line 244 | { 245 | line = rest; 246 | } 247 | line 248 | } 249 | other.map_or(false, |other| { 250 | strip_indents(line) != strip_indents(other) 251 | }) 252 | }; 253 | let line_style = if !diff(prev_line) && !diff(next_line) { 254 | Some(&unchanged_line_style) 255 | } else { 256 | None 257 | }; 258 | line_style 259 | .map(TextOp::PushStyles) 260 | .into_iter() 261 | .chain(line.iter().copied()) 262 | .chain(line_style.map(TextOp::PopStyles)) 263 | }, 264 | ); 265 | 266 | let pretty::HtmlSnippet { 267 | head_deduplicatable_elements: fragment_head, 268 | body: fragment_body, 269 | } = lines 270 | .map(Either::Left) 271 | .intersperse(Either::Right([TextOp::Text("\n")].into_iter())) 272 | .flatten() 273 | .into_internal() 274 | .collect(); 275 | 276 | html.head_deduplicatable_elements.extend(fragment_head); 277 | 278 | html.body += "\n"; 281 | } 282 | } 283 | html.body += "\n"; 284 | } 285 | html.body += "
", version_names.len()).unwrap(); 192 | html.body += &fragment_body; 193 | html.body += "\n"; 279 | html.body += &fragment_body; 280 | html.body += "
"; 286 | 287 | html 288 | } 289 | } 290 | } 291 | } 292 | 293 | impl Versions { 294 | pub fn map_pretty_fragments(self, f: impl Fn(PF) -> PF2) -> Versions { 295 | match self { 296 | Versions::Single(fragment) => Versions::Single(f(fragment)), 297 | Versions::Multiple { version_names, per_row_versions_with_repeat_count } => { 298 | Versions::Multiple { 299 | version_names, 300 | per_row_versions_with_repeat_count: per_row_versions_with_repeat_count 301 | .into_iter() 302 | .map(|versions_with_repeat_count| { 303 | versions_with_repeat_count 304 | .into_iter() 305 | .map(|(fragment, repeat_count)| (f(fragment), repeat_count)) 306 | .collect() 307 | }) 308 | .collect(), 309 | } 310 | } 311 | } 312 | } 313 | } 314 | 315 | /// Tool for adjusting pretty-printed columns, so that their anchors line up 316 | /// (by adding empty lines to whichever side "is behind"). 317 | #[derive(Default)] 318 | struct AnchorAligner<'a> { 319 | merged_lines: Vec, 320 | 321 | /// Current ("rightmost") column's anchor definitions (with indices pointing 322 | /// into `merged_lines`), which the next column will align to. 323 | // 324 | // FIXME(eddyb) does this need additional interning? 325 | anchor_def_to_merged_line_idx: FxIndexMap<&'a str, usize>, 326 | 327 | // FIXME(eddyb) fine-tune this inline size. 328 | // FIXME(eddyb) maybe don't keep most of this data around anyway? 329 | original_columns: SmallVec<[AAColumn<'a>; 4]>, 330 | } 331 | 332 | /// Abstraction for one "physical" line spanning all columns, after alignment. 333 | struct AAMergedLine { 334 | // FIXME(eddyb) fine-tune this inline size. 335 | // FIXME(eddyb) consider using `u32` here? 336 | per_column_line_lengths: SmallVec<[usize; 4]>, 337 | } 338 | 339 | struct AAColumn<'a> { 340 | /// All `TextOp`s in all lines from this column, concatenated together. 341 | text_ops: Vec>, 342 | 343 | /// The length, in `TextOp`s (from `text_ops`), of each line. 344 | // 345 | // FIXME(eddyb) consider using `u32` here? 346 | line_lengths: Vec, 347 | } 348 | 349 | impl<'a> AAColumn<'a> { 350 | /// Reconstruct lines (made of `TextOp`s) from line lengths. 351 | fn lines( 352 | &self, 353 | line_lengths: impl Iterator, 354 | ) -> impl Iterator]> { 355 | let mut next_start = 0; 356 | line_lengths.map(move |len| { 357 | let start = next_start; 358 | let end = start + len; 359 | next_start = end; 360 | &self.text_ops[start..end] 361 | }) 362 | } 363 | } 364 | 365 | // FIXME(eddyb) is this impl the best way? (maybe it should be a inherent method) 366 | impl<'a> FromInternalIterator> for AAColumn<'a> { 367 | fn from_iter(text_ops: T) -> Self 368 | where 369 | T: IntoInternalIterator>, 370 | { 371 | let mut column = AAColumn { text_ops: vec![], line_lengths: vec![0] }; 372 | text_ops.into_internal_iter().for_each(|op| { 373 | if let TextOp::Text("\n") = op { 374 | column.line_lengths.push(0); 375 | } else { 376 | // FIXME(eddyb) this *happens* to be true, 377 | // but the `LineOp`/`TextOp` split could be 378 | // improved to avoid such sanity checks. 379 | if let TextOp::Text(text) = op { 380 | assert!(!text.contains('\n')); 381 | } 382 | column.text_ops.push(op); 383 | *column.line_lengths.last_mut().unwrap() += 1; 384 | } 385 | }); 386 | column 387 | } 388 | } 389 | 390 | #[derive(Copy, Clone)] 391 | struct AAMergedColumn<'a, 'b> { 392 | original_column: &'b AAColumn<'a>, 393 | column_idx: usize, 394 | merged_lines: &'b [AAMergedLine], 395 | } 396 | 397 | impl<'a, 'b> AAMergedColumn<'a, 'b> { 398 | fn lines(&self) -> impl Iterator]> + '_ { 399 | let column_idx = self.column_idx; 400 | let line_lengths = 401 | self.merged_lines.iter().map(move |line| line.per_column_line_lengths[column_idx]); 402 | self.original_column.lines(line_lengths) 403 | } 404 | } 405 | 406 | impl<'a> AnchorAligner<'a> { 407 | /// Flatten all columns to `TextOp`s (including line separators). 408 | fn merged_columns(&self) -> impl Iterator> { 409 | self.original_columns.iter().enumerate().map(|(column_idx, original_column)| { 410 | let mut merged_lines = &self.merged_lines[..]; 411 | 412 | // Trim all trailing lines that are empty in this column. 413 | while let Some((last, before_last)) = merged_lines.split_last() { 414 | if last.per_column_line_lengths[column_idx] > 0 { 415 | break; 416 | } 417 | merged_lines = before_last; 418 | } 419 | 420 | AAMergedColumn { original_column, column_idx, merged_lines } 421 | }) 422 | } 423 | 424 | /// Merge `new_column` into the current set of columns, aligning as many 425 | /// anchors as possible, between it, and the most recent column. 426 | fn add_column_and_align_anchors(&mut self, new_column: AAColumn<'a>) { 427 | // NOTE(eddyb) "old" and "new" are used to refer to the two columns being 428 | // aligned, but "old" maps to the *merged* lines, not its original ones. 429 | 430 | let old_lines = mem::take(&mut self.merged_lines); 431 | let old_anchor_def_to_line_idx = mem::take(&mut self.anchor_def_to_merged_line_idx); 432 | 433 | // Index all the anchor definitions in the new column. 434 | let mut new_anchor_def_to_line_idx = FxIndexMap::default(); 435 | for (new_line_idx, new_line_text_ops) in 436 | new_column.lines(new_column.line_lengths.iter().copied()).enumerate() 437 | { 438 | for op in new_line_text_ops { 439 | if let TextOp::PushAnchor { is_def: true, anchor } = *op { 440 | new_anchor_def_to_line_idx.entry(anchor).or_insert(new_line_idx); 441 | } 442 | } 443 | } 444 | 445 | // Find all the possible anchor alignments (i.e. anchors defined in both 446 | // "old" and "new") as pairs of line indices in "old" and "new". 447 | // 448 | // HACK(eddyb) the order is given by the "new" line index, implicitly. 449 | // FIXME(eddyb) fine-tune this inline size. 450 | let common_anchors: SmallVec<[_; 8]> = new_anchor_def_to_line_idx 451 | .iter() 452 | .filter_map(|(anchor, &new_line_idx)| { 453 | Some((*old_anchor_def_to_line_idx.get(anchor)?, new_line_idx)) 454 | }) 455 | .collect(); 456 | 457 | // Fast-path: if all the "old" line indices are already in (increasing) 458 | // order (i.e. "monotonic"), they can all be used directly for alignment. 459 | let is_already_monotonic = { 460 | // FIXME(eddyb) should be `.is_sorted_by_key(|&(old_line_idx, _)| old_line_idx)` 461 | // but that slice method is still unstable. 462 | common_anchors.windows(2).all(|w| w[0].0 <= w[1].0) 463 | }; 464 | let monotonic_common_anchors = if is_already_monotonic { 465 | common_anchors 466 | } else { 467 | // FIXME(eddyb) this could maybe avoid all the unnecessary allocations. 468 | longest_increasing_subsequence::lis(&common_anchors) 469 | .into_iter() 470 | .map(|i| common_anchors[i]) 471 | .collect() 472 | }; 473 | 474 | // Allocate space for the merge of "old" and "new". 475 | let mut merged_lines = Vec::with_capacity({ 476 | // Cheap conservative estimate, based on the last anchor (i.e. the 477 | // final position of the last anchor is *at least* `min_before_last`). 478 | let &(old_last, new_last) = monotonic_common_anchors.last().unwrap_or(&(0, 0)); 479 | let min_before_last = old_last.max(new_last); 480 | let after_last = 481 | (old_lines.len() - old_last).max(new_column.line_lengths.len() - new_last); 482 | (min_before_last + after_last).next_power_of_two() 483 | }); 484 | 485 | // Build the merged lines using (partially) lockstep iteration to pull 486 | // the relevant data out of either side, and update "new" line indices. 487 | let mut old_lines = old_lines.into_iter().enumerate().peekable(); 488 | let mut new_lines = new_column.line_lengths.iter().copied().enumerate().peekable(); 489 | let mut monotonic_common_anchors = monotonic_common_anchors.into_iter().peekable(); 490 | let mut fixup_new_to_merged = new_anchor_def_to_line_idx.values_mut().peekable(); 491 | while old_lines.len() > 0 || new_lines.len() > 0 { 492 | let old_line_idx = old_lines.peek().map(|&(i, _)| i); 493 | let new_line_idx = new_lines.peek().map(|&(i, _)| i); 494 | let mut next_anchor = monotonic_common_anchors.peek().copied(); 495 | 496 | // Discard anchor alignments that have been used already, and also 497 | // any others that cannot be relevant anymore - this can occur when 498 | // multiple anchors coincide on the same line. 499 | while let Some((anchor_old, anchor_new)) = next_anchor { 500 | // NOTE(eddyb) noop anchors (i.e. those describing an alignment 501 | // between "old" and "new", which has already beeing reached) 502 | // are not considered "relevant" here, and "misalignments" are 503 | // preferred instead - the outcome is mostly identical to always 504 | // eagerly processing noop anchors, except when another anchor 505 | // is overlapping (in only one of "old" or "new"), as it will 506 | // only get get processed if the noop one is skipped first. 507 | let relevant = match (old_line_idx, new_line_idx) { 508 | (Some(old), Some(new)) => old < anchor_old || new < anchor_new, 509 | _ => false, 510 | }; 511 | if relevant { 512 | break; 513 | } 514 | monotonic_common_anchors.next().unwrap(); 515 | next_anchor = monotonic_common_anchors.peek().copied(); 516 | } 517 | 518 | // Figure out which side has to wait, to align an upcoming anchor. 519 | let (old_at_anchor, new_at_anchor) = 520 | next_anchor.map_or((false, false), |(anchor_old, anchor_new)| { 521 | ( 522 | old_line_idx.map_or(false, |old| old == anchor_old), 523 | new_line_idx.map_or(false, |new| new == anchor_new), 524 | ) 525 | }); 526 | let old_line = if old_at_anchor && !new_at_anchor { 527 | // Pausing "old", waiting for "new". 528 | None 529 | } else { 530 | old_lines.next().map(|(_, old_line)| old_line) 531 | }; 532 | let new_line_len = if !old_at_anchor && new_at_anchor { 533 | // Pausing "new", waiting for "old". 534 | None 535 | } else { 536 | new_lines.next().map(|(_, new_line_len)| new_line_len) 537 | }; 538 | 539 | // When the "new" side is advanced, that "sets" the merged line index 540 | // of the consumed line, which can then be used for fixing up indices. 541 | if new_line_len.is_some() { 542 | let new_line_idx = new_line_idx.unwrap(); 543 | let merged_line_idx = merged_lines.len(); 544 | while fixup_new_to_merged.peek().map(|i| **i) == Some(new_line_idx) { 545 | *fixup_new_to_merged.next().unwrap() = merged_line_idx; 546 | } 547 | } 548 | 549 | let new_line_len = new_line_len.unwrap_or(0); 550 | let merged_line = match old_line { 551 | Some(mut line) => { 552 | line.per_column_line_lengths.push(new_line_len); 553 | line 554 | } 555 | None => AAMergedLine { 556 | per_column_line_lengths: (0..self.original_columns.len()) 557 | .map(|_| 0) 558 | .chain([new_line_len]) 559 | .collect(), 560 | }, 561 | }; 562 | merged_lines.push(merged_line); 563 | } 564 | 565 | self.merged_lines = merged_lines; 566 | self.anchor_def_to_merged_line_idx = new_anchor_def_to_line_idx; 567 | self.original_columns.push(new_column); 568 | } 569 | } 570 | --------------------------------------------------------------------------------