Initial public commit of regalloc2.

This commit is contained in:
Chris Fallin
2021-04-13 16:40:21 -07:00
parent 41841996c8
commit 8e923b0ad9
27 changed files with 7814 additions and 0 deletions

0
.empty
View File

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
Cargo.lock
target/
.*.swp
*~

27
Cargo.toml Normal file
View File

@@ -0,0 +1,27 @@
[package]
name = "regalloc2"
version = "0.0.1"
authors = ["Chris Fallin <chris@cfallin.org>", "Mozilla SpiderMonkey Developers"]
edition = "2018"
license = "Apache-2.0 WITH LLVM-exception AND MPL-2.0"
description = "Backtracking register allocator ported from IonMonkey"
repository = "https://github.com/cfallin/regalloc2"
[dependencies]
log = { version = "0.4.8", default-features = false }
smallvec = "1.6.1"
# keep this in sync with libfuzzer_sys's crate version:
arbitrary = "^0.4.6"
rand = "0.8"
rand_chacha = "0.3"
env_logger = "*"
[dev-dependencies]
criterion = "0.3"
[profile.release]
debug = true
[[bench]]
name = "regalloc"
harness = false

220
LICENSE Normal file
View File

@@ -0,0 +1,220 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
--- LLVM Exceptions to the Apache 2.0 License ----
As an exception, if, as a result of your compiling your source code, portions
of this Software are embedded into an Object form of such source code, you
may redistribute such embedded portions in such Object form without complying
with the conditions of Sections 4(a), 4(b) and 4(d) of the License.
In addition, if you combine or link compiled forms of this Software with
software that is licensed under the GPLv2 ("Combined Software") and if a
court of competent jurisdiction determines that the patent provision (Section
3), the indemnity provision (Section 9) or other Section of the License
conflicts with the conditions of the GPLv2, you may retroactively and
prospectively choose to deem waived or otherwise exclude such Section(s) of
the License, but only in their entirety and only with respect to the Combined
Software.

159
README.md Normal file
View File

@@ -0,0 +1,159 @@
## regalloc2: another register allocator
This is a register allocator that started life as, and is about 75%
still, a port of IonMonkey's backtracking register allocator to
Rust. The data structures and invariants have been simplified a little
bit, and the interfaces made a little more generic and reusable. In
addition, it contains substantial amounts of testing infrastructure
(fuzzing harnesses and checkers) that does not exist in the original
IonMonkey allocator.
### Design Overview
TODO
- SSA with blockparams
- Operands with constraints, and clobbers, and reused regs; contrast
with regalloc.rs approach of vregs and pregs and many moves that get
coalesced/elided
### Differences from IonMonkey Backtracking Allocator
There are a number of differences between the [IonMonkey
allocator](https://searchfox.org/mozilla-central/source/js/src/jit/BacktrackingAllocator.cpp)
and this one:
* Most significantly, there are [fuzz/fuzz_targets/](many different
fuzz targets) that exercise the allocator, including a full symbolic
checker (`ion_checker` target) based on the [symbolic checker in
regalloc.rs](https://cfallin.org/blog/2021/03/15/cranelift-isel-3/)
and, e.g., a targetted fuzzer for the parallel move-resolution
algorithm (`moves`) and the SSA generator used for generating cases
for the other fuzz targets (`ssagen`).
* The data-structure invariants are simplified. While the IonMonkey
allocator allowed for LiveRanges and Bundles to overlap in certain
cases, this allocator sticks to a strict invariant: ranges do not
overlap in bundles, and bundles do not overlap. There are other
examples too: e.g., the definition of minimal bundles is very simple
and does not depend on scanning the code at all. In general, we
should be able to state simple invariants and see by inspection (as
well as fuzzing -- see above) that they hold.
* Many of the algorithms in the IonMonkey allocator are built with
helper functions that do linear scans. These "small quadratic" loops
are likely not a huge issue in practice, but nevertheless have the
potential to be in corner cases. As much as possible, all work in
this allocator is done in linear scans. For example, bundle
splitting is done in a single compound scan over a bundle, ranges in
the bundle, and a sorted list of split-points.
* There are novel schemes for solving certain interesting design
challenges. One example: in IonMonkey, liveranges are connected
across blocks by, when reaching one end of a control-flow edge in a
scan, doing a lookup of the allocation at the other end. This is in
principle a linear lookup (so quadratic overall). We instead
generate a list of "half-moves", keyed on the edge and from/to
vregs, with each holding one of the allocations. By sorting and then
scanning this list, we can generate all edge moves in one linear
scan. There are a number of other examples of simplifications: for
example, we handle multiple conflicting
physical-register-constrained uses of a vreg in a single instruction
by recording a copy to do in a side-table, then removing constraints
for the core regalloc. Ion instead has to tweak its definition of
minimal bundles and create two liveranges that overlap (!) to
represent the two uses.
* Using block parameters rather than phi-nodes significantly
simplifies handling of inter-block data movement. IonMonkey had to
special-case phis in many ways because they are actually quite
weird: their uses happen semantically in other blocks, and their
defs happen in parallel at the top of the block. Block parameters
naturally and explicitly reprsent these semantics in a direct way.
* The allocator supports irreducible control flow and arbitrary block
ordering (its only CFG requirement is that critical edges are
split). It handles loops during live-range computation in a way that
is similar in spirit to IonMonkey's allocator -- in a single pass,
when we discover a loop, we just mark the whole loop as a liverange
for values live at the top of the loop -- but we find the loop body
without the fixpoint workqueue loop that IonMonkey uses, instead
doing a single linear scan for backedges and finding the minimal
extent that covers all intermingled loops. In order to support
arbitrary block order and irreducible control flow, we relax the
invariant that the first liverange for a vreg always starts at its
def; instead, the def can happen anywhere, and a liverange may
overapproximate. It turns out this is not too hard to handle and is
a more robust invariant. (It also means that non-SSA code *may* not
be too hard to adapt to, though I haven't seriously thought about
this.)
### Rough Performance Comparison with Regalloc.rs
The allocator has not yet been wired up to a suitable compiler backend
(such as Cranelift) to perform a true apples-to-apples compile-time
and runtime comparison. However, we can get some idea of compile speed
by running suitable test cases through the allocator and measuring
*throughput*: that is, instructions per second for which registers are
allocated.
To do so, I measured the `qsort2` benchmark in
[regalloc.rs](https://github.com/bytecodealliance/regalloc.rs),
register-allocated with default options in that crate's backtracking
allocator, using the Criterion benchmark framework to measure ~620K
instructions per second:
```plain
benches/0 time: [365.68 us 367.36 us 369.04 us]
thrpt: [617.82 Kelem/s 620.65 Kelem/s 623.49 Kelem/s]
```
I then measured three different fuzztest-SSA-generator test cases in
this allocator, `regalloc2`, measuring between 1.05M and 2.3M
instructions per second (closer to the former for larger functions):
```plain
==== 459 instructions
benches/0 time: [424.46 us 425.65 us 426.59 us]
thrpt: [1.0760 Melem/s 1.0784 Melem/s 1.0814 Melem/s]
==== 225 instructions
benches/1 time: [213.05 us 213.28 us 213.54 us]
thrpt: [1.0537 Melem/s 1.0549 Melem/s 1.0561 Melem/s]
Found 1 outliers among 100 measurements (1.00%)
1 (1.00%) high mild
==== 21 instructions
benches/2 time: [9.0495 us 9.0571 us 9.0641 us]
thrpt: [2.3168 Melem/s 2.3186 Melem/s 2.3206 Melem/s]
Found 4 outliers among 100 measurements (4.00%)
2 (2.00%) high mild
2 (2.00%) high severe
```
Though not apples-to-apples (SSA vs. non-SSA, completely different
code only with similar length), this is at least some evidence that
`regalloc2` is likely to lead to at least a compile-time improvement
when used in e.g. Cranelift.
### License
Unless otherwise specified, code in this crate is licensed under the Apache 2.0
License with LLVM Exception. This license text can be found in the file
`LICENSE`.
Files in the `src/ion/` directory are directly ported from original C++ code in
IonMonkey, a part of the Firefox codebase. Parts of `src/lib.rs` are also
definitions that are directly translated from this original code. As a result,
these files are derivative works and are covered by the Mozilla Public License
(MPL) 2.0, as described in license headers in those files. Please see the
notices in relevant files for links to the original IonMonkey source files from
which they have been translated/derived. The MPL text can be found in
`src/ion/LICENSE`.
Parts of the code are derived from regalloc.rs: in particular,
`src/checker.rs` and `src/domtree.rs`. This crate has the same license
as regalloc.rs, so the license on these files does not differ.

56
benches/regalloc.rs Normal file
View File

@@ -0,0 +1,56 @@
//! Criterion-based benchmark target that computes insts/second for
//! arbitrary inputs.
use arbitrary::{Arbitrary, Unstructured};
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
use regalloc2::fuzzing::func::{machine_env, Func};
use regalloc2::ion;
use regalloc2::Function;
fn create_random_func(seed: u64, size: usize) -> Func {
let mut bytes: Vec<u8> = vec![];
bytes.resize(size, 0);
let mut rng = ChaCha8Rng::seed_from_u64(seed);
rng.fill(&mut bytes[..]);
loop {
let mut u = Unstructured::new(&bytes[..]);
match Func::arbitrary(&mut u) {
Ok(f) => {
return f;
}
Err(arbitrary::Error::NotEnoughData) => {
let len = bytes.len();
bytes.resize(len + 1024, 0);
rng.fill(&mut bytes[len..]);
}
Err(e) => panic!("unexpected error: {:?}", e),
}
}
}
fn run_regalloc(c: &mut Criterion) {
const SIZE: usize = 1000 * 1000;
env_logger::init();
let env = machine_env();
let mut group = c.benchmark_group("benches");
for iter in 0..3 {
let func = create_random_func(iter, SIZE);
eprintln!("==== {} instructions", func.insts());
group.throughput(Throughput::Elements(func.insts() as u64));
group.bench_with_input(BenchmarkId::from_parameter(iter), &iter, |b, _| {
b.iter(|| {
// For fair comparison with regalloc.rs, which needs
// to clone its Func on every alloc, we clone
// too. Seems to make a few percent difference.
let func = func.clone();
ion::run(&func, &env).expect("regalloc did not succeed");
});
});
}
group.finish();
}
criterion_group!(benches, run_regalloc);
criterion_main!(benches);

4
fuzz/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
target
corpus
artifacts

54
fuzz/Cargo.toml Normal file
View File

@@ -0,0 +1,54 @@
[package]
name = "regalloc2-fuzz"
version = "0.0.0"
authors = ["Chris Fallin <chris@cfallin.org>"]
license = "MPL-2.0 AND Apache-2.0 WITH LLVM-exception"
publish = false
edition = "2018"
[package.metadata]
cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.3"
arbitrary = { version = "^0.4.6", features = ["derive"] }
log = { version = "0.4.8", default-features = false }
env_logger = "0.8.3"
[dependencies.regalloc2]
path = ".."
# Prevent this from interfering with workspaces
[workspace]
members = ["."]
[[bin]]
name = "domtree"
path = "fuzz_targets/domtree.rs"
test = false
doc = false
[[bin]]
name = "ssagen"
path = "fuzz_targets/ssagen.rs"
test = false
doc = false
[[bin]]
name = "ion"
path = "fuzz_targets/ion.rs"
test = false
doc = false
[[bin]]
name = "moves"
path = "fuzz_targets/moves.rs"
test = false
doc = false
[[bin]]
name = "ion_checker"
path = "fuzz_targets/ion_checker.rs"
test = false
doc = false

View File

@@ -0,0 +1,128 @@
#![no_main]
use libfuzzer_sys::arbitrary::{Arbitrary, Result, Unstructured};
use libfuzzer_sys::fuzz_target;
use std::collections::HashSet;
use regalloc2::{domtree, postorder, Block};
#[derive(Clone, Debug)]
struct CFG {
num_blocks: usize,
preds: Vec<Vec<Block>>,
succs: Vec<Vec<Block>>,
}
impl Arbitrary for CFG {
fn arbitrary(u: &mut Unstructured) -> Result<CFG> {
let num_blocks = u.int_in_range(1..=1000)?;
let mut succs = vec![];
for _ in 0..num_blocks {
let mut block_succs = vec![];
for _ in 0..u.int_in_range(0..=5)? {
block_succs.push(Block::new(u.int_in_range(0..=(num_blocks - 1))?));
}
succs.push(block_succs);
}
let mut preds = vec![];
for _ in 0..num_blocks {
preds.push(vec![]);
}
for from in 0..num_blocks {
for succ in &succs[from] {
preds[succ.index()].push(Block::new(from));
}
}
Ok(CFG {
num_blocks,
preds,
succs,
})
}
}
#[derive(Clone, Debug)]
struct Path {
blocks: Vec<Block>,
}
impl Path {
fn choose_from_cfg(cfg: &CFG, u: &mut Unstructured) -> Result<Path> {
let succs = u.int_in_range(0..=(2 * cfg.num_blocks))?;
let mut block = Block::new(0);
let mut blocks = vec![];
blocks.push(block);
for _ in 0..succs {
if cfg.succs[block.index()].is_empty() {
break;
}
block = *u.choose(&cfg.succs[block.index()])?;
blocks.push(block);
}
Ok(Path { blocks })
}
}
fn check_idom_violations(idom: &[Block], path: &Path) {
// "a dom b" means that any path from the entry block through the CFG that
// contains a and b will contain a before b.
//
// To test this, for any given block b_i, we have the set S of b_0 .. b_{i-1},
// and we walk up the domtree from b_i to get all blocks that dominate b_i;
// each such block must appear in S. (Otherwise, we have a counterexample
// for which dominance says it should appear in the path prefix, but it does
// not.)
let mut visited = HashSet::new();
visited.insert(Block::new(0));
for block in &path.blocks {
let mut parent = idom[block.index()];
let mut domset = HashSet::new();
domset.insert(*block);
loop {
assert!(parent.is_valid());
assert!(visited.contains(&parent));
domset.insert(parent);
let next = idom[parent.index()];
if next == parent {
break;
}
parent = next;
}
// Check that `dominates()` returns true for every block in domset,
// and false for every other block.
for domblock in 0..idom.len() {
let domblock = Block::new(domblock);
assert_eq!(domset.contains(&domblock), domtree::dominates(idom, domblock, *block));
}
visited.insert(*block);
}
}
#[derive(Clone, Debug)]
struct TestCase {
cfg: CFG,
path: Path,
}
impl Arbitrary for TestCase {
fn arbitrary(u: &mut Unstructured) -> Result<TestCase> {
let cfg = CFG::arbitrary(u)?;
let path = Path::choose_from_cfg(&cfg, u)?;
Ok(TestCase {
cfg,
path,
})
}
}
fuzz_target!(|testcase: TestCase| {
let postord = postorder::calculate(testcase.cfg.num_blocks, Block::new(0), |block| {
&testcase.cfg.succs[block.index()]
});
let idom = domtree::calculate(
testcase.cfg.num_blocks,
|block| &testcase.cfg.preds[block.index()],
&postord[..],
Block::new(0),
);
check_idom_violations(&idom[..], &testcase.path);
});

11
fuzz/fuzz_targets/ion.rs Normal file
View File

@@ -0,0 +1,11 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use regalloc2::fuzzing::func::Func;
fuzz_target!(|func: Func| {
let _ = env_logger::try_init();
log::debug!("func:\n{:?}", func);
let env = regalloc2::fuzzing::func::machine_env();
let _out = regalloc2::ion::run(&func, &env).expect("regalloc did not succeed");
});

View File

@@ -0,0 +1,39 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use libfuzzer_sys::arbitrary::{Arbitrary, Unstructured, Result};
use regalloc2::fuzzing::func::{Func, Options};
use regalloc2::checker::Checker;
#[derive(Clone, Debug)]
struct TestCase {
func: Func,
}
impl Arbitrary for TestCase {
fn arbitrary(u: &mut Unstructured) -> Result<TestCase> {
Ok(TestCase {
func: Func::arbitrary_with_options(u, &Options {
reused_inputs: true,
fixed_regs: true,
clobbers: true,
control_flow: true,
reducible: false,
block_params: true,
always_local_uses: false,
})?,
})
}
}
fuzz_target!(|testcase: TestCase| {
let func = testcase.func;
let _ = env_logger::try_init();
log::debug!("func:\n{:?}", func);
let env = regalloc2::fuzzing::func::machine_env();
let out = regalloc2::ion::run(&func, &env).expect("regalloc did not succeed");
let mut checker = Checker::new(&func);
checker.prepare(&out);
checker.run().expect("checker failed");
});

View File

@@ -0,0 +1,76 @@
#![no_main]
use libfuzzer_sys::arbitrary::{Arbitrary, Result, Unstructured};
use libfuzzer_sys::fuzz_target;
use regalloc2::moves::ParallelMoves;
use regalloc2::{Allocation, PReg, RegClass};
use std::collections::HashSet;
#[derive(Clone, Debug)]
struct TestCase {
moves: Vec<(Allocation, Allocation)>,
}
impl Arbitrary for TestCase {
fn arbitrary(u: &mut Unstructured) -> Result<Self> {
let mut ret = TestCase { moves: vec![] };
let mut written = HashSet::new();
while bool::arbitrary(u)? {
let reg1 = u.int_in_range(0..=30)?;
let reg2 = u.int_in_range(0..=30)?;
if written.contains(&reg2) {
break;
}
written.insert(reg2);
ret.moves.push((
Allocation::reg(PReg::new(reg1, RegClass::Int)),
Allocation::reg(PReg::new(reg2, RegClass::Int)),
));
}
Ok(ret)
}
}
fuzz_target!(|testcase: TestCase| {
let _ = env_logger::try_init();
let scratch = Allocation::reg(PReg::new(31, RegClass::Int));
let mut par = ParallelMoves::new(scratch);
for &(src, dst) in &testcase.moves {
par.add(src, dst);
}
let moves = par.resolve();
// Compute the final source reg for each dest reg in the original
// parallel-move set.
let mut final_src_per_dest: Vec<Option<usize>> = vec![None; 32];
for &(src, dst) in &testcase.moves {
if let (Some(preg_src), Some(preg_dst)) = (src.as_reg(), dst.as_reg()) {
final_src_per_dest[preg_dst.hw_enc()] = Some(preg_src.hw_enc());
}
}
// Simulate the sequence of moves.
let mut regfile: Vec<Option<usize>> = vec![None; 32];
for i in 0..32 {
regfile[i] = Some(i);
}
for (src, dst) in moves {
if let (Some(preg_src), Some(preg_dst)) = (src.as_reg(), dst.as_reg()) {
let data = regfile[preg_src.hw_enc()];
regfile[preg_dst.hw_enc()] = data;
} else {
panic!("Bad allocation in move list");
}
}
// Assert that the expected register-moves occurred.
// N.B.: range up to 31 (not 32) to skip scratch register.
for i in 0..31 {
if let Some(orig_src) = final_src_per_dest[i] {
assert_eq!(regfile[i], Some(orig_src));
} else {
// Should be untouched.
assert_eq!(regfile[i], Some(i));
}
}
});

View File

@@ -0,0 +1,35 @@
#![no_main]
use libfuzzer_sys::arbitrary::{Arbitrary, Result, Unstructured};
use libfuzzer_sys::fuzz_target;
use regalloc2::cfg::CFGInfo;
use regalloc2::fuzzing::func::{Func, Options};
use regalloc2::ssa::validate_ssa;
#[derive(Debug)]
struct TestCase {
f: Func,
}
impl Arbitrary for TestCase {
fn arbitrary(u: &mut Unstructured) -> Result<Self> {
Ok(TestCase {
f: Func::arbitrary_with_options(
u,
&Options {
reused_inputs: true,
fixed_regs: true,
clobbers: true,
control_flow: true,
reducible: false,
always_local_uses: false,
},
)?,
})
}
}
fuzz_target!(|t: TestCase| {
let cfginfo = CFGInfo::new(&t.f);
validate_ssa(&t.f, &cfginfo).expect("invalid SSA");
});

45
src/bin/test.rs Normal file
View File

@@ -0,0 +1,45 @@
use arbitrary::{Arbitrary, Unstructured};
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
use regalloc2::fuzzing::func::{machine_env, Func};
use regalloc2::ion;
use regalloc2::Function;
fn create_random_func(seed: u64, size: usize) -> Func {
let mut bytes: Vec<u8> = vec![];
bytes.resize(size, 0);
let mut rng = ChaCha8Rng::seed_from_u64(seed);
rng.fill(&mut bytes[..]);
loop {
let mut u = Unstructured::new(&bytes[..]);
match Func::arbitrary(&mut u) {
Ok(f) => {
return f;
}
Err(arbitrary::Error::NotEnoughData) => {
let len = bytes.len();
bytes.resize(len + 1024, 0);
rng.fill(&mut bytes[len..]);
}
Err(e) => panic!("unexpected error: {:?}", e),
}
}
}
fn main() {
const SIZE: usize = 1000 * 1000;
env_logger::init();
let env = machine_env();
for iter in 0..3 {
let func = create_random_func(iter, SIZE);
eprintln!("==== {} instructions", func.insts());
let mut stats: ion::Stats = ion::Stats::default();
for i in 0..1000 {
let out = ion::run(&func, &env).expect("regalloc did not succeed");
if i == 0 {
stats = out.stats;
}
}
eprintln!("Stats: {:?}", stats);
}
}

139
src/bitvec.rs Normal file
View File

@@ -0,0 +1,139 @@
//! Bit vectors.
use smallvec::{smallvec, SmallVec};
/// A conceptually infinite-length bitvector that allows bitwise operations and
/// iteration over set bits efficiently.
#[derive(Clone, Debug)]
pub struct BitVec {
bits: SmallVec<[u64; 2]>,
}
const BITS_PER_WORD: usize = 64;
impl BitVec {
pub fn new() -> Self {
Self { bits: smallvec![] }
}
pub fn with_capacity(len: usize) -> Self {
let words = (len + BITS_PER_WORD - 1) / BITS_PER_WORD;
Self {
bits: SmallVec::with_capacity(words),
}
}
#[inline(never)]
fn ensure_idx(&mut self, word: usize) {
let mut target_len = std::cmp::max(2, self.bits.len());
while word >= target_len {
target_len *= 2;
}
self.bits.resize(target_len, 0);
}
#[inline(always)]
pub fn set(&mut self, idx: usize, val: bool) {
let word = idx / BITS_PER_WORD;
let bit = idx % BITS_PER_WORD;
if val {
if word >= self.bits.len() {
self.ensure_idx(word);
}
self.bits[word] |= 1 << bit;
} else {
if word < self.bits.len() {
self.bits[word] &= !(1 << bit);
}
}
}
#[inline(always)]
pub fn get(&mut self, idx: usize) -> bool {
let word = idx / BITS_PER_WORD;
let bit = idx % BITS_PER_WORD;
if word >= self.bits.len() {
false
} else {
(self.bits[word] & (1 << bit)) != 0
}
}
pub fn or(&mut self, other: &Self) {
if other.bits.is_empty() {
return;
}
let last_idx = other.bits.len() - 1;
self.ensure_idx(last_idx);
for (self_word, other_word) in self.bits.iter_mut().zip(other.bits.iter()) {
*self_word |= *other_word;
}
}
pub fn and(&mut self, other: &Self) {
if other.bits.len() < self.bits.len() {
self.bits.truncate(other.bits.len());
}
for (self_word, other_word) in self.bits.iter_mut().zip(other.bits.iter()) {
*self_word &= *other_word;
}
}
pub fn iter<'a>(&'a self) -> SetBitsIter<'a> {
let cur_word = if self.bits.len() > 0 { self.bits[0] } else { 0 };
SetBitsIter {
words: &self.bits[..],
word_idx: 0,
cur_word,
}
}
}
pub struct SetBitsIter<'a> {
words: &'a [u64],
word_idx: usize,
cur_word: u64,
}
impl<'a> Iterator for SetBitsIter<'a> {
type Item = usize;
fn next(&mut self) -> Option<usize> {
while self.cur_word == 0 {
if self.word_idx + 1 >= self.words.len() {
return None;
}
self.word_idx += 1;
self.cur_word = self.words[self.word_idx];
}
let bitidx = self.cur_word.trailing_zeros();
self.cur_word &= !(1 << bitidx);
Some(self.word_idx * BITS_PER_WORD + bitidx as usize)
}
}
#[cfg(test)]
mod test {
use super::BitVec;
#[test]
fn test_set_bits_iter() {
let mut vec = BitVec::new();
let mut sum = 0;
for i in 0..1024 {
if i % 17 == 0 {
vec.set(i, true);
sum += i;
}
}
let mut checksum = 0;
for bit in vec.iter() {
assert!(bit % 17 == 0);
checksum += bit;
}
assert_eq!(sum, checksum);
}
}

110
src/cfg.rs Normal file
View File

@@ -0,0 +1,110 @@
//! Lightweight CFG analyses.
use crate::{domtree, postorder, Block, Function, Inst, OperandKind, ProgPoint};
#[derive(Clone, Debug)]
pub struct CFGInfo {
/// Postorder traversal of blocks.
pub postorder: Vec<Block>,
/// Domtree parents, indexed by block.
pub domtree: Vec<Block>,
/// For each instruction, the block it belongs to.
pub insn_block: Vec<Block>,
/// For each vreg, the instruction that defines it, if any.
pub vreg_def_inst: Vec<Inst>,
/// For each vreg, the block that defines it as a blockparam, if
/// any. (Every vreg must have a valid entry in either
/// `vreg_def_inst` or `vreg_def_blockparam`.)
pub vreg_def_blockparam: Vec<(Block, u32)>,
/// For each block, the first instruction.
pub block_entry: Vec<ProgPoint>,
/// For each block, the last instruction.
pub block_exit: Vec<ProgPoint>,
/// For each block, what is its position in its successor's preds,
/// if it has a single successor?
///
/// (Because we require split critical edges, we always either have a single
/// successor (which itself may have multiple preds), or we have multiple
/// successors but each successor itself has only one pred; so we can store
/// just one value per block and always know any block's position in its
/// successors' preds lists.)
pub pred_pos: Vec<usize>,
}
impl CFGInfo {
pub fn new<F: Function>(f: &F) -> CFGInfo {
let postorder =
postorder::calculate(f.blocks(), f.entry_block(), |block| f.block_succs(block));
let domtree = domtree::calculate(
f.blocks(),
|block| f.block_preds(block),
&postorder[..],
f.entry_block(),
);
let mut insn_block = vec![Block::invalid(); f.insts()];
let mut vreg_def_inst = vec![Inst::invalid(); f.num_vregs()];
let mut vreg_def_blockparam = vec![(Block::invalid(), 0); f.num_vregs()];
let mut block_entry = vec![ProgPoint::before(Inst::invalid()); f.blocks()];
let mut block_exit = vec![ProgPoint::before(Inst::invalid()); f.blocks()];
let mut pred_pos = vec![0; f.blocks()];
for block in 0..f.blocks() {
let block = Block::new(block);
for (i, param) in f.block_params(block).iter().enumerate() {
vreg_def_blockparam[param.vreg()] = (block, i as u32);
}
for inst in f.block_insns(block).iter() {
insn_block[inst.index()] = block;
for operand in f.inst_operands(inst) {
match operand.kind() {
OperandKind::Def => {
vreg_def_inst[operand.vreg().vreg()] = inst;
}
_ => {}
}
}
}
block_entry[block.index()] = ProgPoint::before(f.block_insns(block).first());
block_exit[block.index()] = ProgPoint::after(f.block_insns(block).last());
if f.block_preds(block).len() > 1 {
for (i, &pred) in f.block_preds(block).iter().enumerate() {
// Assert critical edge condition.
assert_eq!(
f.block_succs(pred).len(),
1,
"Edge {} -> {} is critical",
pred.index(),
block.index(),
);
pred_pos[pred.index()] = i;
}
}
}
CFGInfo {
postorder,
domtree,
insn_block,
vreg_def_inst,
vreg_def_blockparam,
block_entry,
block_exit,
pred_pos,
}
}
pub fn dominates(&self, a: Block, b: Block) -> bool {
domtree::dominates(&self.domtree[..], a, b)
}
/// Return the position of this block in its successor's predecessor list.
///
/// Because the CFG must have split critical edges, we actually do not need
/// to know *which* successor: if there is more than one, then each
/// successor has only one predecessor (that's this block), so the answer is
/// `0` no matter which successor we are considering.
pub fn pred_position(&self, block: Block) -> usize {
self.pred_pos[block.index()]
}
}

615
src/checker.rs Normal file
View File

@@ -0,0 +1,615 @@
/*
* The following code is derived from `lib/src/checker.rs` in the
* regalloc.rs project
* (https://github.com/bytecodealliance/regalloc.rs). regalloc.rs is
* also licensed under Apache-2.0 with the LLVM exception, as the rest
* of regalloc2's non-Ion-derived code is.
*/
//! Checker: verifies that spills/reloads/moves retain equivalent
//! dataflow to original, VReg-based code.
//!
//! The basic idea is that we track symbolic values as they flow
//! through spills and reloads. The symbolic values represent
//! particular virtual registers in the original function body
//! presented to the register allocator. Any instruction in the
//! original function body (i.e., not added by the allocator)
//! conceptually generates a symbolic value "Vn" when storing to (or
//! modifying) a virtual register.
//!
//! Operand policies (fixed register, register, any) are also checked
//! at each operand.
//!
//! The dataflow analysis state at each program point is:
//!
//! - map of: Allocation -> lattice value (top > Vn symbols (unordered) > bottom)
//!
//! And the transfer functions for instructions are:
//!
//! - `Edit::Move` inserted by RA: [ alloc_d := alloc_s ]
//!
//! A[alloc_d] := A[alloc_s]
//!
//! - phi-node [ V_i := phi block_j:V_j, block_k:V_k, ... ]
//! with allocations [ A_i := phi block_j:A_j, block_k:A_k, ... ]
//! (N.B.: phi-nodes are not semantically present in the final
//! machine code, but we include their allocations so that this
//! checker can work)
//!
//! A[A_i] := meet(A_j, A_k, ...)
//!
//! - statement in pre-regalloc function [ V_i := op V_j, V_k, ... ]
//! with allocated form [ A_i := op A_j, A_k, ... ]
//!
//! A[A_i] := `V_i`
//!
//! In other words, a statement, even after allocation, generates
//! a symbol that corresponds to its original virtual-register
//! def.
//!
//! (N.B.: moves in pre-regalloc function fall into this last case
//! -- they are "just another operation" and generate a new
//! symbol)
//!
//! At control-flow join points, the symbols meet using a very simple
//! lattice meet-function: two different symbols in the same
//! allocation meet to "conflicted"; otherwise, the symbol meets with
//! itself to produce itself (reflexivity).
//!
//! To check correctness, we first find the dataflow fixpoint with the
//! above lattice and transfer/meet functions. Then, at each op, we
//! examine the dataflow solution at the preceding program point, and
//! check that the allocation for each op arg (input/use) contains the
//! symbol corresponding to the original virtual register specified
//! for this arg.
#![allow(dead_code)]
use crate::{
Allocation, AllocationKind, Block, Edit, Function, Inst, InstPosition, Operand, OperandKind,
OperandPolicy, OperandPos, Output, ProgPoint, VReg,
};
use std::collections::{HashMap, VecDeque};
use std::default::Default;
use std::hash::Hash;
use std::result::Result;
use log::debug;
/// A set of errors detected by the regalloc checker.
#[derive(Clone, Debug)]
pub struct CheckerErrors {
errors: Vec<CheckerError>,
}
/// A single error detected by the regalloc checker.
#[derive(Clone, Debug)]
pub enum CheckerError {
MissingAllocation {
inst: Inst,
op: Operand,
},
UnknownValueInAllocation {
inst: Inst,
op: Operand,
alloc: Allocation,
},
ConflictedValueInAllocation {
inst: Inst,
op: Operand,
alloc: Allocation,
},
IncorrectValueInAllocation {
inst: Inst,
op: Operand,
alloc: Allocation,
actual: VReg,
},
PolicyViolated {
inst: Inst,
op: Operand,
alloc: Allocation,
},
AllocationIsNotReg {
inst: Inst,
op: Operand,
alloc: Allocation,
},
AllocationIsNotFixedReg {
inst: Inst,
op: Operand,
alloc: Allocation,
},
AllocationIsNotReuse {
inst: Inst,
op: Operand,
alloc: Allocation,
expected_alloc: Allocation,
},
}
/// Abstract state for an allocation.
///
/// Forms a lattice with \top (`Unknown`), \bot (`Conflicted`), and a
/// number of mutually unordered value-points in between, one per real
/// or virtual register. Any two different registers meet to \bot.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum CheckerValue {
/// "top" value: this storage slot has no known value.
Unknown,
/// "bottom" value: this storage slot has a conflicted value.
Conflicted,
/// Reg: this storage slot has a value that originated as a def
/// into the given virtual register.
///
/// The boolean flag indicates whether the value is
/// reference-typed.
Reg(VReg, bool),
}
impl Default for CheckerValue {
fn default() -> CheckerValue {
CheckerValue::Unknown
}
}
impl CheckerValue {
/// Meet function of the abstract-interpretation value lattice.
fn meet(&self, other: &CheckerValue) -> CheckerValue {
match (self, other) {
(&CheckerValue::Unknown, _) => *other,
(_, &CheckerValue::Unknown) => *self,
(&CheckerValue::Conflicted, _) => *self,
(_, &CheckerValue::Conflicted) => *other,
(&CheckerValue::Reg(r1, ref1), &CheckerValue::Reg(r2, ref2)) if r1 == r2 => {
CheckerValue::Reg(r1, ref1 || ref2)
}
_ => {
log::debug!("{:?} and {:?} meet to Conflicted", self, other);
CheckerValue::Conflicted
}
}
}
}
/// State that steps through program points as we scan over the instruction stream.
#[derive(Clone, Debug, PartialEq, Eq)]
struct CheckerState {
allocations: HashMap<Allocation, CheckerValue>,
}
impl Default for CheckerState {
fn default() -> CheckerState {
CheckerState {
allocations: HashMap::new(),
}
}
}
impl std::fmt::Display for CheckerValue {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
CheckerValue::Unknown => write!(f, "?"),
CheckerValue::Conflicted => write!(f, "!"),
CheckerValue::Reg(r, _) => write!(f, "{}", r),
}
}
}
fn merge_map<K: Copy + Clone + PartialEq + Eq + Hash>(
into: &mut HashMap<K, CheckerValue>,
from: &HashMap<K, CheckerValue>,
) {
for (k, v) in from {
let into_v = into.entry(*k).or_insert(Default::default());
let merged = into_v.meet(v);
*into_v = merged;
}
}
impl CheckerState {
/// Create a new checker state.
fn new() -> CheckerState {
Default::default()
}
/// Merge this checker state with another at a CFG join-point.
fn meet_with(&mut self, other: &CheckerState) {
merge_map(&mut self.allocations, &other.allocations);
}
fn check_val(
&self,
inst: Inst,
op: Operand,
alloc: Allocation,
val: CheckerValue,
allocs: &[Allocation],
) -> Result<(), CheckerError> {
if alloc == Allocation::none() {
return Err(CheckerError::MissingAllocation { inst, op });
}
match val {
CheckerValue::Unknown => {
return Err(CheckerError::UnknownValueInAllocation { inst, op, alloc });
}
CheckerValue::Conflicted => {
return Err(CheckerError::ConflictedValueInAllocation { inst, op, alloc });
}
CheckerValue::Reg(r, _) if r != op.vreg() => {
return Err(CheckerError::IncorrectValueInAllocation {
inst,
op,
alloc,
actual: r,
});
}
_ => {}
}
self.check_policy(inst, op, alloc, allocs)?;
Ok(())
}
/// Check an instruction against this state. This must be called
/// twice: once with `InstPosition::Before`, and once with
/// `InstPosition::After` (after updating state with defs).
fn check(&self, pos: InstPosition, checkinst: &CheckerInst) -> Result<(), CheckerError> {
match checkinst {
&CheckerInst::Op {
inst,
ref operands,
ref allocs,
..
} => {
// Skip Use-checks at the After point if there are any
// reused inputs: the Def which reuses the input
// happens early.
let has_reused_input = operands
.iter()
.any(|op| matches!(op.policy(), OperandPolicy::Reuse(_)));
if has_reused_input && pos == InstPosition::After {
return Ok(());
}
// For each operand, check (i) that the allocation
// contains the expected vreg, and (ii) that it meets
// the requirements of the OperandPolicy.
for (op, alloc) in operands.iter().zip(allocs.iter()) {
let is_here = match (op.pos(), pos) {
(OperandPos::Before, InstPosition::Before)
| (OperandPos::Both, InstPosition::Before) => true,
(OperandPos::After, InstPosition::After)
| (OperandPos::Both, InstPosition::After) => true,
_ => false,
};
if !is_here {
continue;
}
if op.kind() == OperandKind::Def {
continue;
}
let val = self
.allocations
.get(alloc)
.cloned()
.unwrap_or(Default::default());
debug!(
"checker: checkinst {:?}: op {:?}, alloc {:?}, checker value {:?}",
checkinst, op, alloc, val
);
self.check_val(inst, *op, *alloc, val, allocs)?;
}
}
_ => {}
}
Ok(())
}
/// Update according to instruction.
fn update(&mut self, checkinst: &CheckerInst) {
match checkinst {
&CheckerInst::Move { into, from } => {
let val = self
.allocations
.get(&from)
.cloned()
.unwrap_or(Default::default());
debug!(
"checker: checkinst {:?} updating: move {:?} -> {:?} val {:?}",
checkinst, from, into, val
);
self.allocations.insert(into, val);
}
&CheckerInst::Op {
ref operands,
ref allocs,
..
} => {
for (op, alloc) in operands.iter().zip(allocs.iter()) {
if op.kind() != OperandKind::Def {
continue;
}
self.allocations
.insert(*alloc, CheckerValue::Reg(op.vreg(), false));
}
}
&CheckerInst::BlockParams {
ref vregs,
ref allocs,
..
} => {
for (vreg, alloc) in vregs.iter().zip(allocs.iter()) {
self.allocations
.insert(*alloc, CheckerValue::Reg(*vreg, false));
}
}
}
}
fn check_policy(
&self,
inst: Inst,
op: Operand,
alloc: Allocation,
allocs: &[Allocation],
) -> Result<(), CheckerError> {
match op.policy() {
OperandPolicy::Any => {}
OperandPolicy::Reg => {
if alloc.kind() != AllocationKind::Reg {
return Err(CheckerError::AllocationIsNotReg { inst, op, alloc });
}
}
OperandPolicy::FixedReg(preg) => {
if alloc != Allocation::reg(preg) {
return Err(CheckerError::AllocationIsNotFixedReg { inst, op, alloc });
}
}
OperandPolicy::Reuse(idx) => {
if alloc.kind() != AllocationKind::Reg {
return Err(CheckerError::AllocationIsNotReg { inst, op, alloc });
}
if alloc != allocs[idx] {
return Err(CheckerError::AllocationIsNotReuse {
inst,
op,
alloc,
expected_alloc: allocs[idx],
});
}
}
}
Ok(())
}
}
/// An instruction representation in the checker's BB summary.
#[derive(Clone, Debug)]
pub(crate) enum CheckerInst {
/// A move between allocations (these could be registers or
/// spillslots).
Move { into: Allocation, from: Allocation },
/// A regular instruction with fixed use and def slots. Contains
/// both the original operands (as given to the regalloc) and the
/// allocation results.
Op {
inst: Inst,
operands: Vec<Operand>,
allocs: Vec<Allocation>,
},
/// The top of a block with blockparams. We define the given vregs
/// into the given allocations.
BlockParams {
block: Block,
vregs: Vec<VReg>,
allocs: Vec<Allocation>,
},
}
#[derive(Debug)]
pub struct Checker<'a, F: Function> {
f: &'a F,
bb_in: HashMap<Block, CheckerState>,
bb_insts: HashMap<Block, Vec<CheckerInst>>,
}
impl<'a, F: Function> Checker<'a, F> {
/// Create a new checker for the given function, initializing CFG
/// info immediately. The client should call the `add_*()`
/// methods to add abstract instructions to each BB before
/// invoking `run()` to check for errors.
pub fn new(f: &'a F) -> Checker<'a, F> {
let mut bb_in = HashMap::new();
let mut bb_insts = HashMap::new();
for block in 0..f.blocks() {
let block = Block::new(block);
bb_in.insert(block, Default::default());
bb_insts.insert(block, vec![]);
}
Checker { f, bb_in, bb_insts }
}
/// Build the list of checker instructions based on the given func
/// and allocation results.
pub fn prepare(&mut self, out: &Output) {
debug!("checker: out = {:?}", out);
// For each original instruction, create an `Op`.
let mut last_inst = None;
let mut insert_idx = 0;
for block in 0..self.f.blocks() {
let block = Block::new(block);
for inst in self.f.block_insns(block).iter() {
assert!(last_inst.is_none() || inst > last_inst.unwrap());
last_inst = Some(inst);
// Any inserted edits before instruction.
self.handle_edits(block, out, &mut insert_idx, ProgPoint::before(inst));
// Instruction itself.
let operands: Vec<_> = self.f.inst_operands(inst).iter().cloned().collect();
let allocs: Vec<_> = out.inst_allocs(inst).iter().cloned().collect();
let checkinst = CheckerInst::Op {
inst,
operands,
allocs,
};
debug!("checker: adding inst {:?}", checkinst);
self.bb_insts.get_mut(&block).unwrap().push(checkinst);
// Any inserted edits after instruction.
self.handle_edits(block, out, &mut insert_idx, ProgPoint::after(inst));
}
}
}
fn handle_edits(&mut self, block: Block, out: &Output, idx: &mut usize, pos: ProgPoint) {
while *idx < out.edits.len() && out.edits[*idx].0 <= pos {
let &(edit_pos, ref edit) = &out.edits[*idx];
*idx += 1;
if edit_pos < pos {
continue;
}
debug!("checker: adding edit {:?} at pos {:?}", edit, pos);
match edit {
&Edit::Move { from, to, .. } => {
self.bb_insts
.get_mut(&block)
.unwrap()
.push(CheckerInst::Move { into: to, from });
}
&Edit::BlockParams {
ref vregs,
ref allocs,
} => {
let inst = CheckerInst::BlockParams {
block,
vregs: vregs.clone(),
allocs: allocs.clone(),
};
self.bb_insts.get_mut(&block).unwrap().push(inst);
}
}
}
}
/// Perform the dataflow analysis to compute checker state at each BB entry.
fn analyze(&mut self) {
let mut queue = VecDeque::new();
queue.push_back(self.f.entry_block());
while !queue.is_empty() {
let block = queue.pop_front().unwrap();
let mut state = self.bb_in.get(&block).cloned().unwrap();
debug!("analyze: block {} has state {:?}", block.index(), state);
for inst in self.bb_insts.get(&block).unwrap() {
state.update(inst);
debug!("analyze: inst {:?} -> state {:?}", inst, state);
}
for &succ in self.f.block_succs(block) {
let cur_succ_in = self.bb_in.get(&succ).unwrap();
let mut new_state = state.clone();
new_state.meet_with(cur_succ_in);
let changed = &new_state != cur_succ_in;
if changed {
debug!(
"analyze: block {} state changed from {:?} to {:?}; pushing onto queue",
succ.index(),
cur_succ_in,
new_state
);
self.bb_in.insert(succ, new_state);
queue.push_back(succ);
}
}
}
}
/// Using BB-start state computed by `analyze()`, step the checker state
/// through each BB and check each instruction's register allocations
/// for errors.
fn find_errors(&self) -> Result<(), CheckerErrors> {
let mut errors = vec![];
for (block, input) in &self.bb_in {
let mut state = input.clone();
for inst in self.bb_insts.get(block).unwrap() {
if let Err(e) = state.check(InstPosition::Before, inst) {
debug!("Checker error: {:?}", e);
errors.push(e);
}
state.update(inst);
if let Err(e) = state.check(InstPosition::After, inst) {
debug!("Checker error: {:?}", e);
errors.push(e);
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(CheckerErrors { errors })
}
}
/// Find any errors, returning `Err(CheckerErrors)` with all errors found
/// or `Ok(())` otherwise.
pub fn run(mut self) -> Result<(), CheckerErrors> {
self.analyze();
let result = self.find_errors();
debug!("=== CHECKER RESULT ===");
fn print_state(state: &CheckerState) {
let mut s = vec![];
for (alloc, state) in &state.allocations {
s.push(format!("{} := {}", alloc, state));
}
debug!(" {{ {} }}", s.join(", "))
}
for bb in 0..self.f.blocks() {
let bb = Block::new(bb);
debug!("block{}:", bb.index());
let insts = self.bb_insts.get(&bb).unwrap();
let mut state = self.bb_in.get(&bb).unwrap().clone();
print_state(&state);
for inst in insts {
match inst {
&CheckerInst::Op {
inst,
ref operands,
ref allocs,
} => {
debug!(" inst{}: {:?} ({:?})", inst.index(), operands, allocs);
}
&CheckerInst::Move { from, into } => {
debug!(" {} -> {}", from, into);
}
&CheckerInst::BlockParams {
ref vregs,
ref allocs,
..
} => {
let mut args = vec![];
for (vreg, alloc) in vregs.iter().zip(allocs.iter()) {
args.push(format!("{}:{}", vreg, alloc));
}
debug!(" blockparams: {}", args.join(", "));
}
}
state.update(inst);
print_state(&state);
}
}
result
}
}

118
src/domtree.rs Normal file
View File

@@ -0,0 +1,118 @@
/*
* Derives from the dominator tree implementation in regalloc.rs, which is
* licensed under the Apache Public License 2.0 with LLVM Exception. See:
* https://github.com/bytecodealliance/regalloc.rs
*/
// This is an implementation of the algorithm described in
//
// A Simple, Fast Dominance Algorithm
// Keith D. Cooper, Timothy J. Harvey, and Ken Kennedy
// Department of Computer Science, Rice University, Houston, Texas, USA
// TR-06-33870
// https://www.cs.rice.edu/~keith/EMBED/dom.pdf
use crate::Block;
// Helper
fn merge_sets(
idom: &[Block], // map from Block to Block
block_to_rpo: &[Option<u32>],
mut node1: Block,
mut node2: Block,
) -> Block {
while node1 != node2 {
if node1.is_invalid() || node2.is_invalid() {
return Block::invalid();
}
let rpo1 = block_to_rpo[node1.index()].unwrap();
let rpo2 = block_to_rpo[node2.index()].unwrap();
if rpo1 > rpo2 {
node1 = idom[node1.index()];
} else if rpo2 > rpo1 {
node2 = idom[node2.index()];
}
}
assert!(node1 == node2);
node1
}
pub fn calculate<'a, PredFn: Fn(Block) -> &'a [Block]>(
num_blocks: usize,
preds: PredFn,
post_ord: &[Block],
start: Block,
) -> Vec<Block> {
// We have post_ord, which is the postorder sequence.
// Compute maps from RPO to block number and vice-versa.
let mut block_to_rpo = vec![None; num_blocks];
block_to_rpo.resize(num_blocks, None);
for (i, rpo_block) in post_ord.iter().rev().enumerate() {
block_to_rpo[rpo_block.index()] = Some(i as u32);
}
let mut idom = vec![Block::invalid(); num_blocks];
// The start node must have itself as a parent.
idom[start.index()] = start;
let mut changed = true;
while changed {
changed = false;
// Consider blocks in reverse postorder. Skip any that are unreachable.
for &node in post_ord.iter().rev() {
let rponum = block_to_rpo[node.index()].unwrap();
let mut parent = Block::invalid();
for &pred in preds(node).iter() {
let pred_rpo = match block_to_rpo[pred.index()] {
Some(r) => r,
None => {
// Skip unreachable preds.
continue;
}
};
if pred_rpo < rponum {
parent = pred;
break;
}
}
if parent.is_valid() {
for &pred in preds(node).iter() {
if pred == parent {
continue;
}
if idom[pred.index()].is_invalid() {
continue;
}
parent = merge_sets(&idom, &block_to_rpo[..], parent, pred);
}
}
if parent.is_valid() && parent != idom[node.index()] {
idom[node.index()] = parent;
changed = true;
}
}
}
idom
}
pub fn dominates(idom: &[Block], a: Block, mut b: Block) -> bool {
loop {
if a == b {
return true;
}
if b.is_invalid() {
return false;
}
let parent = idom[b.index()];
if b == parent {
return false;
}
b = idom[b.index()];
}
}

542
src/fuzzing/func.rs Normal file
View File

@@ -0,0 +1,542 @@
use crate::{
domtree, postorder, Allocation, Block, Function, Inst, InstRange, MachineEnv, Operand,
OperandKind, OperandPolicy, OperandPos, PReg, RegClass, VReg,
};
use arbitrary::Result as ArbitraryResult;
use arbitrary::{Arbitrary, Unstructured};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum InstOpcode {
Phi,
Op,
Call,
Ret,
Branch,
}
#[derive(Clone, Debug)]
pub struct InstData {
op: InstOpcode,
operands: Vec<Operand>,
clobbers: Vec<PReg>,
}
impl InstData {
pub fn op(def: usize, uses: &[usize]) -> InstData {
let mut operands = vec![Operand::reg_def(VReg::new(def, RegClass::Int))];
for &u in uses {
operands.push(Operand::reg_use(VReg::new(u, RegClass::Int)));
}
InstData {
op: InstOpcode::Op,
operands,
clobbers: vec![],
}
}
pub fn branch(uses: &[usize]) -> InstData {
let mut operands = vec![];
for &u in uses {
operands.push(Operand::reg_use(VReg::new(u, RegClass::Int)));
}
InstData {
op: InstOpcode::Branch,
operands,
clobbers: vec![],
}
}
pub fn ret() -> InstData {
InstData {
op: InstOpcode::Ret,
operands: vec![],
clobbers: vec![],
}
}
}
#[derive(Clone)]
pub struct Func {
insts: Vec<InstData>,
blocks: Vec<InstRange>,
block_preds: Vec<Vec<Block>>,
block_succs: Vec<Vec<Block>>,
block_params: Vec<Vec<VReg>>,
num_vregs: usize,
}
impl Function for Func {
fn insts(&self) -> usize {
self.insts.len()
}
fn blocks(&self) -> usize {
self.blocks.len()
}
fn entry_block(&self) -> Block {
assert!(self.blocks.len() > 0);
Block::new(0)
}
fn block_insns(&self, block: Block) -> InstRange {
self.blocks[block.index()]
}
fn block_succs(&self, block: Block) -> &[Block] {
&self.block_succs[block.index()][..]
}
fn block_preds(&self, block: Block) -> &[Block] {
&self.block_preds[block.index()][..]
}
fn block_params(&self, block: Block) -> &[VReg] {
&self.block_params[block.index()][..]
}
fn is_call(&self, insn: Inst) -> bool {
self.insts[insn.index()].op == InstOpcode::Call
}
fn is_ret(&self, insn: Inst) -> bool {
self.insts[insn.index()].op == InstOpcode::Ret
}
fn is_branch(&self, insn: Inst) -> bool {
self.insts[insn.index()].op == InstOpcode::Branch
}
fn is_safepoint(&self, _: Inst) -> bool {
false
}
fn is_move(&self, _: Inst) -> Option<(VReg, VReg)> {
None
}
fn inst_operands(&self, insn: Inst) -> &[Operand] {
&self.insts[insn.index()].operands[..]
}
fn inst_clobbers(&self, insn: Inst) -> &[PReg] {
&self.insts[insn.index()].clobbers[..]
}
fn num_vregs(&self) -> usize {
self.num_vregs
}
fn spillslot_size(&self, regclass: RegClass, _: VReg) -> usize {
match regclass {
RegClass::Int => 1,
RegClass::Float => 2,
}
}
}
struct FuncBuilder {
postorder: Vec<Block>,
idom: Vec<Block>,
f: Func,
insts_per_block: Vec<Vec<InstData>>,
}
impl FuncBuilder {
fn new() -> Self {
FuncBuilder {
postorder: vec![],
idom: vec![],
f: Func {
block_preds: vec![],
block_succs: vec![],
block_params: vec![],
insts: vec![],
blocks: vec![],
num_vregs: 0,
},
insts_per_block: vec![],
}
}
pub fn add_block(&mut self) -> Block {
let b = Block::new(self.f.blocks.len());
self.f
.blocks
.push(InstRange::forward(Inst::new(0), Inst::new(0)));
self.f.block_preds.push(vec![]);
self.f.block_succs.push(vec![]);
self.f.block_params.push(vec![]);
self.insts_per_block.push(vec![]);
b
}
pub fn add_inst(&mut self, block: Block, data: InstData) {
self.insts_per_block[block.index()].push(data);
}
pub fn add_edge(&mut self, from: Block, to: Block) {
self.f.block_succs[from.index()].push(to);
self.f.block_preds[to.index()].push(from);
}
pub fn set_block_params(&mut self, block: Block, params: &[VReg]) {
self.f.block_params[block.index()] = params.iter().cloned().collect();
}
fn compute_doms(&mut self) {
self.postorder = postorder::calculate(self.f.blocks.len(), Block::new(0), |block| {
&self.f.block_succs[block.index()][..]
});
self.idom = domtree::calculate(
self.f.blocks.len(),
|block| &self.f.block_preds[block.index()][..],
&self.postorder[..],
Block::new(0),
);
}
fn finalize(mut self) -> Func {
for (blocknum, blockrange) in self.f.blocks.iter_mut().enumerate() {
let begin_inst = self.f.insts.len();
for inst in &self.insts_per_block[blocknum] {
self.f.insts.push(inst.clone());
}
let end_inst = self.f.insts.len();
*blockrange = InstRange::forward(Inst::new(begin_inst), Inst::new(end_inst));
}
self.f
}
}
impl Arbitrary for OperandPolicy {
fn arbitrary(u: &mut Unstructured) -> ArbitraryResult<Self> {
Ok(*u.choose(&[OperandPolicy::Any, OperandPolicy::Reg])?)
}
}
fn choose_dominating_block(
idom: &[Block],
mut block: Block,
allow_self: bool,
u: &mut Unstructured,
) -> ArbitraryResult<Block> {
assert!(block.is_valid());
let orig_block = block;
loop {
if (allow_self || block != orig_block) && bool::arbitrary(u)? {
break;
}
if idom[block.index()] == block {
break;
}
block = idom[block.index()];
assert!(block.is_valid());
}
let block = if block != orig_block || allow_self {
block
} else {
Block::invalid()
};
Ok(block)
}
#[derive(Clone, Copy, Debug)]
pub struct Options {
pub reused_inputs: bool,
pub fixed_regs: bool,
pub clobbers: bool,
pub control_flow: bool,
pub reducible: bool,
pub block_params: bool,
pub always_local_uses: bool,
}
impl std::default::Default for Options {
fn default() -> Self {
Options {
reused_inputs: false,
fixed_regs: false,
clobbers: false,
control_flow: true,
reducible: false,
block_params: true,
always_local_uses: false,
}
}
}
impl Arbitrary for Func {
fn arbitrary(u: &mut Unstructured) -> ArbitraryResult<Func> {
Func::arbitrary_with_options(u, &Options::default())
}
}
impl Func {
pub fn arbitrary_with_options(u: &mut Unstructured, opts: &Options) -> ArbitraryResult<Func> {
// General strategy:
// 1. Create an arbitrary CFG.
// 2. Create a list of vregs to define in each block.
// 3. Define some of those vregs in each block as blockparams.f.
// 4. Populate blocks with ops that define the rest of the vregs.
// - For each use, choose an available vreg: either one
// already defined (via blockparam or inst) in this block,
// or one defined in a dominating block.
let mut builder = FuncBuilder::new();
for _ in 0..u.int_in_range(1..=100)? {
builder.add_block();
}
let num_blocks = builder.f.blocks.len();
// Generate a CFG. Create a "spine" of either single blocks,
// with links to the next; or fork patterns, with the left
// fork linking to the next and the right fork in `out_blocks`
// to be connected below. This creates an arbitrary CFG with
// split critical edges, which is a property that we require
// for the regalloc.
let mut from = 0;
let mut out_blocks = vec![];
let mut in_blocks = vec![];
// For reducibility, if selected: enforce strict nesting of backedges
let mut max_backedge_src = 0;
let mut min_backedge_dest = num_blocks;
while from < num_blocks {
in_blocks.push(from);
if num_blocks > 3 && from < num_blocks - 3 && bool::arbitrary(u)? && opts.control_flow {
// To avoid critical edges, we use from+1 as an edge
// block, and advance `from` an extra block; `from+2`
// will be the next normal iteration.
builder.add_edge(Block::new(from), Block::new(from + 1));
builder.add_edge(Block::new(from), Block::new(from + 2));
builder.add_edge(Block::new(from + 2), Block::new(from + 3));
out_blocks.push(from + 1);
from += 2;
} else if from < num_blocks - 1 {
builder.add_edge(Block::new(from), Block::new(from + 1));
}
from += 1;
}
for pred in out_blocks {
let mut succ = *u.choose(&in_blocks[..])?;
if opts.reducible && (pred >= succ) {
if pred < max_backedge_src || succ > min_backedge_dest {
// If the chosen edge would result in an
// irreducible CFG, just make this a diamond
// instead.
succ = pred + 2;
} else {
max_backedge_src = pred;
min_backedge_dest = succ;
}
}
builder.add_edge(Block::new(pred), Block::new(succ));
}
builder.compute_doms();
for block in 0..num_blocks {
builder.f.block_preds[block].clear();
}
for block in 0..num_blocks {
for &succ in &builder.f.block_succs[block] {
builder.f.block_preds[succ.index()].push(Block::new(block));
}
}
builder.compute_doms();
let mut vregs_by_block = vec![];
let mut vregs_by_block_to_be_defined = vec![];
let mut block_params = vec![vec![]; num_blocks];
for block in 0..num_blocks {
let mut vregs = vec![];
for _ in 0..u.int_in_range(5..=15)? {
let vreg = VReg::new(builder.f.num_vregs, RegClass::Int);
builder.f.num_vregs += 1;
vregs.push(vreg);
}
vregs_by_block.push(vregs.clone());
vregs_by_block_to_be_defined.push(vec![]);
let mut max_block_params = u.int_in_range(0..=std::cmp::min(3, vregs.len() / 3))?;
for &vreg in &vregs {
if block > 0 && opts.block_params && bool::arbitrary(u)? && max_block_params > 0 {
block_params[block].push(vreg);
max_block_params -= 1;
} else {
vregs_by_block_to_be_defined.last_mut().unwrap().push(vreg);
}
}
vregs_by_block_to_be_defined.last_mut().unwrap().reverse();
builder.set_block_params(Block::new(block), &block_params[block][..]);
}
for block in 0..num_blocks {
let mut avail = block_params[block].clone();
let mut remaining_nonlocal_uses = u.int_in_range(0..=3)?;
while let Some(vreg) = vregs_by_block_to_be_defined[block].pop() {
let def_policy = OperandPolicy::arbitrary(u)?;
let def_pos = if bool::arbitrary(u)? {
OperandPos::Before
} else {
OperandPos::After
};
let mut operands = vec![Operand::new(vreg, def_policy, OperandKind::Def, def_pos)];
let mut allocations = vec![Allocation::none()];
for _ in 0..u.int_in_range(0..=3)? {
let vreg = if avail.len() > 0
&& (opts.always_local_uses
|| remaining_nonlocal_uses == 0
|| bool::arbitrary(u)?)
{
*u.choose(&avail[..])?
} else if !opts.always_local_uses {
let def_block = choose_dominating_block(
&builder.idom[..],
Block::new(block),
/* allow_self = */ false,
u,
)?;
if !def_block.is_valid() {
// No vregs already defined, and no pred blocks that dominate us
// (perhaps we are the entry block): just stop generating inputs.
break;
}
remaining_nonlocal_uses -= 1;
*u.choose(&vregs_by_block[def_block.index()])?
} else {
break;
};
let use_policy = OperandPolicy::arbitrary(u)?;
operands.push(Operand::new(
vreg,
use_policy,
OperandKind::Use,
OperandPos::Before,
));
allocations.push(Allocation::none());
}
let mut clobbers: Vec<PReg> = vec![];
if operands.len() > 1 && opts.reused_inputs && bool::arbitrary(u)? {
// Make the def a reused input.
let op = operands[0];
assert_eq!(op.kind(), OperandKind::Def);
let reused = u.int_in_range(1..=(operands.len() - 1))?;
operands[0] = Operand::new(
op.vreg(),
OperandPolicy::Reuse(reused),
op.kind(),
OperandPos::After,
);
} else if opts.fixed_regs && bool::arbitrary(u)? {
// Pick an operand and make it a fixed reg.
let fixed_reg = PReg::new(u.int_in_range(0..=30)?, RegClass::Int);
let i = u.int_in_range(0..=(operands.len() - 1))?;
let op = operands[i];
operands[i] = Operand::new(
op.vreg(),
OperandPolicy::FixedReg(fixed_reg),
op.kind(),
op.pos(),
);
} else if opts.clobbers && bool::arbitrary(u)? {
for _ in 0..u.int_in_range(0..=5)? {
let reg = u.int_in_range(0..=30)?;
if clobbers.iter().any(|r| r.hw_enc() == reg) {
break;
}
clobbers.push(PReg::new(reg, RegClass::Int));
}
}
let op = *u.choose(&[InstOpcode::Op, InstOpcode::Call])?;
builder.add_inst(
Block::new(block),
InstData {
op,
operands,
clobbers,
},
);
avail.push(vreg);
}
// Define the branch with blockparam args that must end
// the block.
if builder.f.block_succs[block].len() > 0 {
let mut args = vec![];
for &succ in &builder.f.block_succs[block] {
for _ in 0..builder.f.block_params[succ.index()].len() {
let dom_block = choose_dominating_block(
&builder.idom[..],
Block::new(block),
false,
u,
)?;
let vreg = if dom_block.is_valid() && bool::arbitrary(u)? {
u.choose(&vregs_by_block[dom_block.index()][..])?
} else {
u.choose(&avail[..])?
};
args.push(vreg.vreg());
}
}
builder.add_inst(Block::new(block), InstData::branch(&args[..]));
} else {
builder.add_inst(Block::new(block), InstData::ret());
}
}
Ok(builder.finalize())
}
}
impl std::fmt::Debug for Func {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{{\n")?;
for (i, blockrange) in self.blocks.iter().enumerate() {
let succs = self.block_succs[i]
.iter()
.map(|b| b.index())
.collect::<Vec<_>>();
let preds = self.block_preds[i]
.iter()
.map(|b| b.index())
.collect::<Vec<_>>();
let params = self.block_params[i]
.iter()
.map(|v| format!("v{}", v.vreg()))
.collect::<Vec<_>>()
.join(", ");
write!(
f,
" block{}({}): # succs:{:?} preds:{:?}\n",
i, params, succs, preds
)?;
for inst in blockrange.iter() {
write!(
f,
" inst{}: {:?} ops:{:?} clobber:{:?}\n",
inst.index(),
self.insts[inst.index()].op,
self.insts[inst.index()].operands,
self.insts[inst.index()].clobbers
)?;
}
}
write!(f, "}}\n")?;
Ok(())
}
}
pub fn machine_env() -> MachineEnv {
// Reg 31 is the scratch reg.
let regs: Vec<PReg> = (0..31).map(|i| PReg::new(i, RegClass::Int)).collect();
let regs_by_class: Vec<Vec<PReg>> = vec![regs.clone(), vec![]];
let scratch_by_class: Vec<PReg> =
vec![PReg::new(31, RegClass::Int), PReg::new(0, RegClass::Float)];
MachineEnv {
regs,
regs_by_class,
scratch_by_class,
}
}

3
src/fuzzing/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
//! Utilities for fuzzing.
pub mod func;

176
src/index.rs Normal file
View File

@@ -0,0 +1,176 @@
#[macro_export]
macro_rules! define_index {
($ix:ident) => {
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct $ix(pub u32);
impl $ix {
#[inline(always)]
pub fn new(i: usize) -> Self {
Self(i as u32)
}
#[inline(always)]
pub fn index(self) -> usize {
assert!(self.is_valid());
self.0 as usize
}
#[inline(always)]
pub fn invalid() -> Self {
Self(u32::MAX)
}
#[inline(always)]
pub fn is_invalid(self) -> bool {
self == Self::invalid()
}
#[inline(always)]
pub fn is_valid(self) -> bool {
self != Self::invalid()
}
#[inline(always)]
pub fn next(self) -> $ix {
assert!(self.is_valid());
Self(self.0 + 1)
}
#[inline(always)]
pub fn prev(self) -> $ix {
assert!(self.is_valid());
Self(self.0 - 1)
}
}
impl crate::index::ContainerIndex for $ix {}
};
}
pub trait ContainerIndex: Clone + Copy + std::fmt::Debug + PartialEq + Eq {}
pub trait ContainerComparator {
type Ix: ContainerIndex;
fn compare(&self, a: Self::Ix, b: Self::Ix) -> std::cmp::Ordering;
}
define_index!(Inst);
define_index!(Block);
#[derive(Clone, Copy, Debug)]
pub struct InstRange(Inst, Inst, bool);
impl InstRange {
#[inline(always)]
pub fn forward(from: Inst, to: Inst) -> Self {
assert!(from.index() <= to.index());
InstRange(from, to, true)
}
#[inline(always)]
pub fn backward(from: Inst, to: Inst) -> Self {
assert!(from.index() >= to.index());
InstRange(to, from, false)
}
#[inline(always)]
pub fn first(self) -> Inst {
assert!(self.len() > 0);
if self.is_forward() {
self.0
} else {
self.1.prev()
}
}
#[inline(always)]
pub fn last(self) -> Inst {
assert!(self.len() > 0);
if self.is_forward() {
self.1.prev()
} else {
self.0
}
}
#[inline(always)]
pub fn rest(self) -> InstRange {
assert!(self.len() > 0);
if self.is_forward() {
InstRange::forward(self.0.next(), self.1)
} else {
InstRange::backward(self.1.prev(), self.0)
}
}
#[inline(always)]
pub fn len(self) -> usize {
self.1.index() - self.0.index()
}
#[inline(always)]
pub fn is_forward(self) -> bool {
self.2
}
#[inline(always)]
pub fn rev(self) -> Self {
Self(self.0, self.1, !self.2)
}
#[inline(always)]
pub fn iter(self) -> InstRangeIter {
InstRangeIter(self)
}
}
#[derive(Clone, Copy, Debug)]
pub struct InstRangeIter(InstRange);
impl Iterator for InstRangeIter {
type Item = Inst;
#[inline(always)]
fn next(&mut self) -> Option<Inst> {
if self.0.len() == 0 {
None
} else {
let ret = self.0.first();
self.0 = self.0.rest();
Some(ret)
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_inst_range() {
let range = InstRange::forward(Inst::new(0), Inst::new(0));
assert_eq!(range.len(), 0);
let range = InstRange::forward(Inst::new(0), Inst::new(5));
assert_eq!(range.first().index(), 0);
assert_eq!(range.last().index(), 4);
assert_eq!(range.len(), 5);
assert_eq!(
range.iter().collect::<Vec<_>>(),
vec![
Inst::new(0),
Inst::new(1),
Inst::new(2),
Inst::new(3),
Inst::new(4)
]
);
let range = range.rev();
assert_eq!(range.first().index(), 4);
assert_eq!(range.last().index(), 0);
assert_eq!(range.len(), 5);
assert_eq!(
range.iter().collect::<Vec<_>>(),
vec![
Inst::new(4),
Inst::new(3),
Inst::new(2),
Inst::new(1),
Inst::new(0)
]
);
}
}

373
src/ion/LICENSE Normal file
View File

@@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

3763
src/ion/mod.rs Normal file

File diff suppressed because it is too large Load Diff

780
src/lib.rs Normal file
View File

@@ -0,0 +1,780 @@
/*
* The fellowing license applies to this file, which derives many
* details (register and constraint definitions, for example) from the
* files `BacktrackingAllocator.h`, `BacktrackingAllocator.cpp`,
* `LIR.h`, and possibly definitions in other related files in
* `js/src/jit/`:
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#![allow(dead_code)]
pub mod bitvec;
pub mod cfg;
pub mod domtree;
pub mod ion;
pub mod moves;
pub mod postorder;
pub mod ssa;
#[macro_use]
pub mod index;
pub use index::{Block, Inst, InstRange, InstRangeIter};
pub mod checker;
pub mod fuzzing;
/// Register classes.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum RegClass {
Int = 0,
Float = 1,
}
/// A physical register. Contains a physical register number and a class.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct PReg(u8, RegClass);
impl PReg {
pub const MAX_BITS: usize = 5;
pub const MAX: usize = (1 << Self::MAX_BITS) - 1;
/// Create a new PReg. The `hw_enc` range is 6 bits.
#[inline(always)]
pub fn new(hw_enc: usize, class: RegClass) -> Self {
assert!(hw_enc <= Self::MAX);
PReg(hw_enc as u8, class)
}
/// The physical register number, as encoded by the ISA for the particular register class.
#[inline(always)]
pub fn hw_enc(self) -> usize {
self.0 as usize
}
/// The register class.
#[inline(always)]
pub fn class(self) -> RegClass {
self.1
}
/// Get an index into the (not necessarily contiguous) index space of
/// all physical registers. Allows one to maintain an array of data for
/// all PRegs and index it efficiently.
#[inline(always)]
pub fn index(self) -> usize {
((self.1 as u8 as usize) << 6) | (self.0 as usize)
}
#[inline(always)]
pub fn from_index(index: usize) -> Self {
let class = (index >> 6) & 1;
let class = match class {
0 => RegClass::Int,
1 => RegClass::Float,
_ => unreachable!(),
};
let index = index & Self::MAX;
PReg::new(index, class)
}
#[inline(always)]
pub fn invalid() -> Self {
PReg::new(Self::MAX, RegClass::Int)
}
}
impl std::fmt::Debug for PReg {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"PReg(hw = {}, class = {:?}, index = {})",
self.hw_enc(),
self.class(),
self.index()
)
}
}
impl std::fmt::Display for PReg {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let class = match self.class() {
RegClass::Int => "i",
RegClass::Float => "f",
};
write!(f, "p{}{}", self.hw_enc(), class)
}
}
/// A virtual register. Contains a virtual register number and a class.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct VReg(u32);
impl VReg {
pub const MAX_BITS: usize = 20;
pub const MAX: usize = (1 << Self::MAX_BITS) - 1;
#[inline(always)]
pub fn new(virt_reg: usize, class: RegClass) -> Self {
assert!(virt_reg <= Self::MAX);
VReg(((virt_reg as u32) << 1) | (class as u8 as u32))
}
#[inline(always)]
pub fn vreg(self) -> usize {
(self.0 >> 1) as usize
}
#[inline(always)]
pub fn class(self) -> RegClass {
match self.0 & 1 {
0 => RegClass::Int,
1 => RegClass::Float,
_ => unreachable!(),
}
}
#[inline(always)]
pub fn invalid() -> Self {
VReg::new(Self::MAX, RegClass::Int)
}
}
impl std::fmt::Debug for VReg {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"VReg(vreg = {}, class = {:?})",
self.vreg(),
self.class()
)
}
}
impl std::fmt::Display for VReg {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "v{}", self.vreg())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SpillSlot(u32);
impl SpillSlot {
#[inline(always)]
pub fn new(slot: usize, class: RegClass) -> Self {
assert!(slot < (1 << 24));
SpillSlot((slot as u32) | (class as u8 as u32) << 24)
}
#[inline(always)]
pub fn index(self) -> usize {
(self.0 & 0x00ffffff) as usize
}
#[inline(always)]
pub fn class(self) -> RegClass {
match (self.0 >> 24) as u8 {
0 => RegClass::Int,
1 => RegClass::Float,
_ => unreachable!(),
}
}
#[inline(always)]
pub fn plus(self, offset: usize) -> Self {
SpillSlot::new(self.index() + offset, self.class())
}
}
impl std::fmt::Display for SpillSlot {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "stack{}", self.index())
}
}
/// An `Operand` encodes everything about a mention of a register in
/// an instruction: virtual register number, and any constraint/policy
/// that applies to the register at this program point.
///
/// An Operand may be a use or def (this corresponds to `LUse` and
/// `LAllocation` in Ion).
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct Operand {
/// Bit-pack into 31 bits. This allows a `Reg` to encode an
/// `Operand` or an `Allocation` in 32 bits.
///
/// op-or-alloc:1 pos:2 kind:1 policy:2 class:1 preg:5 vreg:20
bits: u32,
}
impl Operand {
#[inline(always)]
pub fn new(vreg: VReg, policy: OperandPolicy, kind: OperandKind, pos: OperandPos) -> Self {
let (preg_field, policy_field): (u32, u32) = match policy {
OperandPolicy::Any => (0, 0),
OperandPolicy::Reg => (0, 1),
OperandPolicy::FixedReg(preg) => {
assert_eq!(preg.class(), vreg.class());
(preg.hw_enc() as u32, 2)
}
OperandPolicy::Reuse(which) => {
assert!(which <= PReg::MAX);
(which as u32, 3)
}
};
let class_field = vreg.class() as u8 as u32;
let pos_field = pos as u8 as u32;
let kind_field = kind as u8 as u32;
Operand {
bits: vreg.vreg() as u32
| (preg_field << 20)
| (class_field << 25)
| (policy_field << 26)
| (kind_field << 28)
| (pos_field << 29),
}
}
#[inline(always)]
pub fn reg_use(vreg: VReg) -> Self {
Operand::new(
vreg,
OperandPolicy::Reg,
OperandKind::Use,
OperandPos::Before,
)
}
#[inline(always)]
pub fn reg_use_at_end(vreg: VReg) -> Self {
Operand::new(vreg, OperandPolicy::Reg, OperandKind::Use, OperandPos::Both)
}
#[inline(always)]
pub fn reg_def(vreg: VReg) -> Self {
Operand::new(
vreg,
OperandPolicy::Reg,
OperandKind::Def,
OperandPos::After,
)
}
#[inline(always)]
pub fn reg_def_at_start(vreg: VReg) -> Self {
Operand::new(vreg, OperandPolicy::Reg, OperandKind::Def, OperandPos::Both)
}
#[inline(always)]
pub fn reg_temp(vreg: VReg) -> Self {
Operand::new(vreg, OperandPolicy::Reg, OperandKind::Def, OperandPos::Both)
}
#[inline(always)]
pub fn reg_reuse_def(vreg: VReg, idx: usize) -> Self {
Operand::new(
vreg,
OperandPolicy::Reuse(idx),
OperandKind::Def,
OperandPos::Both,
)
}
#[inline(always)]
pub fn reg_fixed_use(vreg: VReg, preg: PReg) -> Self {
Operand::new(
vreg,
OperandPolicy::FixedReg(preg),
OperandKind::Use,
OperandPos::Before,
)
}
#[inline(always)]
pub fn reg_fixed_def(vreg: VReg, preg: PReg) -> Self {
Operand::new(
vreg,
OperandPolicy::FixedReg(preg),
OperandKind::Def,
OperandPos::After,
)
}
#[inline(always)]
pub fn vreg(self) -> VReg {
let vreg_idx = ((self.bits as usize) & VReg::MAX) as usize;
VReg::new(vreg_idx, self.class())
}
#[inline(always)]
pub fn class(self) -> RegClass {
let class_field = (self.bits >> 25) & 1;
match class_field {
0 => RegClass::Int,
1 => RegClass::Float,
_ => unreachable!(),
}
}
#[inline(always)]
pub fn kind(self) -> OperandKind {
let kind_field = (self.bits >> 28) & 1;
match kind_field {
0 => OperandKind::Def,
1 => OperandKind::Use,
_ => unreachable!(),
}
}
#[inline(always)]
pub fn pos(self) -> OperandPos {
let pos_field = (self.bits >> 29) & 3;
match pos_field {
0 => OperandPos::Before,
1 => OperandPos::After,
2 => OperandPos::Both,
_ => unreachable!(),
}
}
#[inline(always)]
pub fn policy(self) -> OperandPolicy {
let policy_field = (self.bits >> 26) & 3;
let preg_field = ((self.bits >> 20) as usize) & PReg::MAX;
match policy_field {
0 => OperandPolicy::Any,
1 => OperandPolicy::Reg,
2 => OperandPolicy::FixedReg(PReg::new(preg_field, self.class())),
3 => OperandPolicy::Reuse(preg_field),
_ => unreachable!(),
}
}
#[inline(always)]
pub fn bits(self) -> u32 {
self.bits
}
#[inline(always)]
pub fn from_bits(bits: u32) -> Self {
Operand { bits }
}
}
impl std::fmt::Debug for Operand {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"Operand(vreg = {:?}, class = {:?}, kind = {:?}, pos = {:?}, policy = {:?})",
self.vreg().vreg(),
self.class(),
self.kind(),
self.pos(),
self.policy()
)
}
}
impl std::fmt::Display for Operand {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"{:?}@{:?}: {} {}",
self.kind(),
self.pos(),
self.vreg(),
self.policy()
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OperandPolicy {
/// Any location is fine (register or stack slot).
Any,
/// Operand must be in a register. Register is read-only for Uses.
Reg,
/// Operand must be in a fixed register.
FixedReg(PReg),
/// On defs only: reuse a use's register. Which use is given by `preg` field.
Reuse(usize),
}
impl std::fmt::Display for OperandPolicy {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Any => write!(f, "any"),
Self::Reg => write!(f, "reg"),
Self::FixedReg(preg) => write!(f, "fixed({})", preg),
Self::Reuse(idx) => write!(f, "reuse({})", idx),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OperandKind {
Def = 0,
Use = 1,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OperandPos {
Before = 0,
After = 1,
Both = 2,
}
/// An Allocation represents the end result of regalloc for an
/// Operand.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Allocation {
/// Bit-pack in 31 bits:
///
/// op-or-alloc:1 kind:2 index:29
bits: u32,
}
impl std::fmt::Debug for Allocation {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"Allocation(kind = {:?}, index = {})",
self.kind(),
self.index()
)
}
}
impl std::fmt::Display for Allocation {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self.kind() {
AllocationKind::None => write!(f, "none"),
AllocationKind::Reg => write!(f, "{}", self.as_reg().unwrap()),
AllocationKind::Stack => write!(f, "{}", self.as_stack().unwrap()),
}
}
}
impl Allocation {
#[inline(always)]
pub(crate) fn new(kind: AllocationKind, index: usize) -> Self {
Self {
bits: ((kind as u8 as u32) << 29) | (index as u32),
}
}
#[inline(always)]
pub fn none() -> Allocation {
Allocation::new(AllocationKind::None, 0)
}
#[inline(always)]
pub fn reg(preg: PReg) -> Allocation {
Allocation::new(AllocationKind::Reg, preg.index())
}
#[inline(always)]
pub fn stack(slot: SpillSlot) -> Allocation {
Allocation::new(AllocationKind::Stack, slot.0 as usize)
}
#[inline(always)]
pub fn kind(self) -> AllocationKind {
match (self.bits >> 29) & 3 {
0 => AllocationKind::None,
1 => AllocationKind::Reg,
2 => AllocationKind::Stack,
_ => unreachable!(),
}
}
#[inline(always)]
pub fn index(self) -> usize {
(self.bits & ((1 << 29) - 1)) as usize
}
#[inline(always)]
pub fn as_reg(self) -> Option<PReg> {
if self.kind() == AllocationKind::Reg {
Some(PReg::from_index(self.index()))
} else {
None
}
}
#[inline(always)]
pub fn as_stack(self) -> Option<SpillSlot> {
if self.kind() == AllocationKind::Stack {
Some(SpillSlot(self.index() as u32))
} else {
None
}
}
#[inline(always)]
pub fn bits(self) -> u32 {
self.bits
}
#[inline(always)]
pub fn from_bits(bits: u32) -> Self {
Self { bits }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u8)]
pub enum AllocationKind {
None = 0,
Reg = 1,
Stack = 2,
}
impl Allocation {
#[inline(always)]
pub fn class(self) -> RegClass {
match self.kind() {
AllocationKind::None => panic!("Allocation::None has no class"),
AllocationKind::Reg => self.as_reg().unwrap().class(),
AllocationKind::Stack => self.as_stack().unwrap().class(),
}
}
}
/// A trait defined by the regalloc client to provide access to its
/// machine-instruction / CFG representation.
pub trait Function {
// -------------
// CFG traversal
// -------------
/// How many instructions are there?
fn insts(&self) -> usize;
/// How many blocks are there?
fn blocks(&self) -> usize;
/// Get the index of the entry block.
fn entry_block(&self) -> Block;
/// Provide the range of instruction indices contained in each block.
fn block_insns(&self, block: Block) -> InstRange;
/// Get CFG successors for a given block.
fn block_succs(&self, block: Block) -> &[Block];
/// Get the CFG predecessors for a given block.
fn block_preds(&self, block: Block) -> &[Block];
/// Get the block parameters for a given block.
fn block_params(&self, block: Block) -> &[VReg];
/// Determine whether an instruction is a call instruction. This is used
/// only for splitting heuristics.
fn is_call(&self, insn: Inst) -> bool;
/// Determine whether an instruction is a return instruction.
fn is_ret(&self, insn: Inst) -> bool;
/// Determine whether an instruction is the end-of-block
/// branch. If so, its operands *must* be the block parameters for
/// each of its block's `block_succs` successor blocks, in order.
fn is_branch(&self, insn: Inst) -> bool;
/// Determine whether an instruction is a safepoint and requires a stackmap.
fn is_safepoint(&self, insn: Inst) -> bool;
/// Determine whether an instruction is a move; if so, return the
/// vregs for (src, dst).
fn is_move(&self, insn: Inst) -> Option<(VReg, VReg)>;
// --------------------------
// Instruction register slots
// --------------------------
/// Get the Operands for an instruction.
fn inst_operands(&self, insn: Inst) -> &[Operand];
/// Get the clobbers for an instruction.
fn inst_clobbers(&self, insn: Inst) -> &[PReg];
/// Get the precise number of `VReg` in use in this function, to allow
/// preallocating data structures. This number *must* be a correct
/// lower-bound, otherwise invalid index failures may happen; it is of
/// course better if it is exact.
fn num_vregs(&self) -> usize;
// --------------
// Spills/reloads
// --------------
/// How many logical spill slots does the given regclass require? E.g., on
/// a 64-bit machine, spill slots may nominally be 64-bit words, but a
/// 128-bit vector value will require two slots. The regalloc will always
/// align on this size.
///
/// This passes the associated virtual register to the client as well,
/// because the way in which we spill a real register may depend on the
/// value that we are using it for. E.g., if a machine has V128 registers
/// but we also use them for F32 and F64 values, we may use a different
/// store-slot size and smaller-operand store/load instructions for an F64
/// than for a true V128.
fn spillslot_size(&self, regclass: RegClass, for_vreg: VReg) -> usize;
/// When providing a spillslot number for a multi-slot spillslot,
/// do we provide the first or the last? This is usually related
/// to which direction the stack grows and different clients may
/// have different preferences.
fn multi_spillslot_named_by_last_slot(&self) -> bool {
false
}
}
/// A position before or after an instruction.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u8)]
pub enum InstPosition {
Before = 0,
After = 1,
}
/// A program point: a single point before or after a given instruction.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ProgPoint {
pub inst: Inst,
pub pos: InstPosition,
}
impl ProgPoint {
pub fn before(inst: Inst) -> Self {
Self {
inst,
pos: InstPosition::Before,
}
}
pub fn after(inst: Inst) -> Self {
Self {
inst,
pos: InstPosition::After,
}
}
pub fn next(self) -> ProgPoint {
match self.pos {
InstPosition::Before => ProgPoint {
inst: self.inst,
pos: InstPosition::After,
},
InstPosition::After => ProgPoint {
inst: self.inst.next(),
pos: InstPosition::Before,
},
}
}
pub fn prev(self) -> ProgPoint {
match self.pos {
InstPosition::Before => ProgPoint {
inst: self.inst.prev(),
pos: InstPosition::After,
},
InstPosition::After => ProgPoint {
inst: self.inst,
pos: InstPosition::Before,
},
}
}
pub fn to_index(self) -> u32 {
debug_assert!(self.inst.index() <= ((1 << 31) - 1));
((self.inst.index() as u32) << 1) | (self.pos as u8 as u32)
}
pub fn from_index(index: u32) -> Self {
let inst = Inst::new((index >> 1) as usize);
let pos = match index & 1 {
0 => InstPosition::Before,
1 => InstPosition::After,
_ => unreachable!(),
};
Self { inst, pos }
}
}
/// An instruction to insert into the program to perform some data movement.
#[derive(Clone, Debug)]
pub enum Edit {
/// Move one allocation to another. Each allocation may be a
/// register or a stack slot (spillslot).
Move { from: Allocation, to: Allocation },
/// Define blockparams' locations. Note that this is not typically
/// turned into machine code, but can be useful metadata (e.g. for
/// the checker).
BlockParams {
vregs: Vec<VReg>,
allocs: Vec<Allocation>,
},
}
/// A machine envrionment tells the register allocator which registers
/// are available to allocate and what register may be used as a
/// scratch register for each class, and some other miscellaneous info
/// as well.
#[derive(Clone, Debug)]
pub struct MachineEnv {
regs: Vec<PReg>,
regs_by_class: Vec<Vec<PReg>>,
scratch_by_class: Vec<PReg>,
}
/// The output of the register allocator.
#[derive(Clone, Debug)]
pub struct Output {
/// How many spillslots are needed in the frame?
pub num_spillslots: usize,
/// Edits (insertions or removals). Guaranteed to be sorted by
/// program point.
pub edits: Vec<(ProgPoint, Edit)>,
/// Allocations for each operand. Mapping from instruction to
/// allocations provided by `inst_alloc_offsets` below.
pub allocs: Vec<Allocation>,
/// Allocation offset in `allocs` for each instruction.
pub inst_alloc_offsets: Vec<u32>,
/// Internal stats from the allocator.
pub stats: ion::Stats,
}
impl Output {
pub fn inst_allocs(&self, inst: Inst) -> &[Allocation] {
let start = self.inst_alloc_offsets[inst.index()] as usize;
let end = if inst.index() + 1 == self.inst_alloc_offsets.len() {
self.allocs.len()
} else {
self.inst_alloc_offsets[inst.index() + 1] as usize
};
&self.allocs[start..end]
}
}
/// An error that prevents allocation.
#[derive(Clone, Debug)]
pub enum RegAllocError {
/// Invalid SSA for given vreg at given inst: multiple defs or
/// illegal use. `inst` may be `Inst::invalid()` if this concerns
/// a block param.
SSA(VReg, Inst),
/// Invalid basic block: does not end in branch/ret, or contains a
/// branch/ret in the middle.
BB(Block),
/// Invalid branch: operand count does not match sum of block
/// params of successor blocks.
Branch(Inst),
}
impl std::fmt::Display for RegAllocError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl std::error::Error for RegAllocError {}
pub fn run<F: Function>(func: &F, env: &MachineEnv) -> Result<Output, RegAllocError> {
ion::run(func, env)
}

199
src/moves.rs Normal file
View File

@@ -0,0 +1,199 @@
use crate::Allocation;
use smallvec::{smallvec, SmallVec};
pub type MoveVec = SmallVec<[(Allocation, Allocation); 16]>;
/// A `ParallelMoves` represents a list of alloc-to-alloc moves that
/// must happen in parallel -- i.e., all reads of sources semantically
/// happen before all writes of destinations, and destinations are
/// allowed to overwrite sources. It can compute a list of sequential
/// moves that will produce the equivalent data movement, possibly
/// using a scratch register if one is necessary.
pub struct ParallelMoves {
parallel_moves: MoveVec,
scratch: Allocation,
}
impl ParallelMoves {
pub fn new(scratch: Allocation) -> Self {
Self {
parallel_moves: smallvec![],
scratch,
}
}
pub fn add(&mut self, from: Allocation, to: Allocation) {
self.parallel_moves.push((from, to));
}
fn sources_overlap_dests(&self) -> bool {
// Assumes `parallel_moves` has already been sorted in `resolve()` below.
for &(_, dst) in &self.parallel_moves {
if self
.parallel_moves
.binary_search_by_key(&dst, |&(src, _)| src)
.is_ok()
{
return true;
}
}
false
}
pub fn resolve(mut self) -> MoveVec {
// Easy case: zero or one move. Just return our vec.
if self.parallel_moves.len() <= 1 {
return self.parallel_moves;
}
// Sort moves by source so that we can efficiently test for
// presence.
self.parallel_moves.sort();
// Do any dests overlap sources? If not, we can also just
// return the list.
if !self.sources_overlap_dests() {
return self.parallel_moves;
}
// General case: some moves overwrite dests that other moves
// read as sources. We'll use a general algorithm.
//
// *Important property*: because we expect that each register
// has only one writer (otherwise the effect of the parallel
// move is undefined), each move can only block one other move
// (with its one source corresponding to the one writer of
// that source). Thus, we *can only have simple cycles*: there
// are no SCCs that are more complex than that. We leverage
// this fact below to avoid having to do a full Tarjan SCC DFS
// (with lowest-index computation, etc.): instead, as soon as
// we find a cycle, we know we have the full cycle and we can
// do a cyclic move sequence and continue.
// Sort moves by destination and check that each destination
// has only one writer.
self.parallel_moves.sort_by_key(|&(_, dst)| dst);
if cfg!(debug) {
let mut last_dst = None;
for &(_, dst) in &self.parallel_moves {
if last_dst.is_some() {
assert!(last_dst.unwrap() != dst);
}
last_dst = Some(dst);
}
}
// Construct a mapping from move indices to moves they must
// come before. Any given move must come before a move that
// overwrites its destination; we have moves sorted by dest
// above so we can efficiently find such a move, if any.
let mut must_come_before: SmallVec<[Option<usize>; 16]> =
smallvec![None; self.parallel_moves.len()];
for (i, &(src, _)) in self.parallel_moves.iter().enumerate() {
if let Ok(move_to_dst_idx) = self
.parallel_moves
.binary_search_by_key(&src, |&(_, dst)| dst)
{
must_come_before[i] = Some(move_to_dst_idx);
}
}
// Do a simple stack-based DFS and emit moves in postorder,
// then reverse at the end for RPO. Unlike Tarjan's SCC
// algorithm, we can emit a cycle as soon as we find one, as
// noted above.
let mut ret: MoveVec = smallvec![];
let mut stack: SmallVec<[usize; 16]> = smallvec![];
let mut visited: SmallVec<[bool; 16]> = smallvec![false; self.parallel_moves.len()];
let mut onstack: SmallVec<[bool; 16]> = smallvec![false; self.parallel_moves.len()];
stack.push(0);
onstack[0] = true;
loop {
if stack.is_empty() {
if let Some(next) = visited.iter().position(|&flag| !flag) {
stack.push(next);
onstack[next] = true;
} else {
break;
}
}
let top = *stack.last().unwrap();
visited[top] = true;
match must_come_before[top] {
None => {
ret.push(self.parallel_moves[top]);
onstack[top] = false;
stack.pop();
while let Some(top) = stack.pop() {
ret.push(self.parallel_moves[top]);
onstack[top] = false;
}
}
Some(next) if visited[next] && !onstack[next] => {
ret.push(self.parallel_moves[top]);
onstack[top] = false;
stack.pop();
while let Some(top) = stack.pop() {
ret.push(self.parallel_moves[top]);
onstack[top] = false;
}
}
Some(next) if !visited[next] && !onstack[next] => {
stack.push(next);
onstack[next] = true;
continue;
}
Some(next) => {
// Found a cycle -- emit a cyclic-move sequence
// for the cycle on the top of stack, then normal
// moves below it. Recall that these moves will be
// reversed in sequence, so from the original
// parallel move set
//
// { B := A, C := B, A := B }
//
// we will generate something like:
//
// A := scratch
// B := A
// C := B
// scratch := C
//
// which will become:
//
// scratch := C
// C := B
// B := A
// A := scratch
let mut last_dst = None;
let mut scratch_src = None;
while let Some(move_idx) = stack.pop() {
onstack[move_idx] = false;
let (mut src, dst) = self.parallel_moves[move_idx];
if last_dst.is_none() {
scratch_src = Some(src);
src = self.scratch;
} else {
assert_eq!(last_dst.unwrap(), src);
}
ret.push((src, dst));
last_dst = Some(dst);
if move_idx == next {
break;
}
}
if let Some(src) = scratch_src {
ret.push((src, self.scratch));
}
}
}
}
ret.reverse();
ret
}
}

51
src/postorder.rs Normal file
View File

@@ -0,0 +1,51 @@
//! Fast postorder computation with no allocations (aside from result).
use crate::Block;
use smallvec::{smallvec, SmallVec};
pub fn calculate<'a, SuccFn: Fn(Block) -> &'a [Block]>(
num_blocks: usize,
entry: Block,
succ_blocks: SuccFn,
) -> Vec<Block> {
let mut ret = vec![];
// State: visited-block map, and explicit DFS stack.
let mut visited = vec![];
visited.resize(num_blocks, false);
struct State<'a> {
block: Block,
succs: &'a [Block],
next_succ: usize,
}
let mut stack: SmallVec<[State; 64]> = smallvec![];
visited[entry.index()] = true;
stack.push(State {
block: entry,
succs: succ_blocks(entry),
next_succ: 0,
});
while let Some(ref mut state) = stack.last_mut() {
// Perform one action: push to new succ, skip an already-visited succ, or pop.
if state.next_succ < state.succs.len() {
let succ = state.succs[state.next_succ];
state.next_succ += 1;
if !visited[succ.index()] {
visited[succ.index()] = true;
stack.push(State {
block: succ,
succs: succ_blocks(succ),
next_succ: 0,
});
}
} else {
ret.push(state.block);
stack.pop();
}
}
ret
}

87
src/ssa.rs Normal file
View File

@@ -0,0 +1,87 @@
//! SSA-related utilities.
use crate::cfg::CFGInfo;
use crate::{Block, Function, Inst, OperandKind, RegAllocError};
pub fn validate_ssa<F: Function>(f: &F, cfginfo: &CFGInfo) -> Result<(), RegAllocError> {
// Walk the blocks in arbitrary order. Check, for every use, that
// the def is either in the same block in an earlier inst, or is
// defined (by inst or blockparam) in some other block that
// dominates this one. Also check that for every block param and
// inst def, that this is the only def.
let mut defined = vec![false; f.num_vregs()];
for block in 0..f.blocks() {
let block = Block::new(block);
for blockparam in f.block_params(block) {
if defined[blockparam.vreg()] {
return Err(RegAllocError::SSA(*blockparam, Inst::invalid()));
}
defined[blockparam.vreg()] = true;
}
for iix in f.block_insns(block).iter() {
let operands = f.inst_operands(iix);
for operand in operands {
match operand.kind() {
OperandKind::Use => {
let def_block = if cfginfo.vreg_def_inst[operand.vreg().vreg()].is_valid() {
cfginfo.insn_block[cfginfo.vreg_def_inst[operand.vreg().vreg()].index()]
} else {
cfginfo.vreg_def_blockparam[operand.vreg().vreg()].0
};
if def_block.is_invalid() {
return Err(RegAllocError::SSA(operand.vreg(), iix));
}
if !cfginfo.dominates(def_block, block) {
return Err(RegAllocError::SSA(operand.vreg(), iix));
}
}
OperandKind::Def => {
if defined[operand.vreg().vreg()] {
return Err(RegAllocError::SSA(operand.vreg(), iix));
}
defined[operand.vreg().vreg()] = true;
}
}
}
}
}
// Check that the length of branch args matches the sum of the
// number of blockparams in their succs, and that the end of every
// block ends in this branch or in a ret, and that there are no
// other branches or rets in the middle of the block.
for block in 0..f.blocks() {
let block = Block::new(block);
let insns = f.block_insns(block);
for insn in insns.iter() {
if insn == insns.last() {
if !(f.is_branch(insn) || f.is_ret(insn)) {
return Err(RegAllocError::BB(block));
}
if f.is_branch(insn) {
let expected = f
.block_succs(block)
.iter()
.map(|&succ| f.block_params(succ).len())
.sum();
if f.inst_operands(insn).len() != expected {
return Err(RegAllocError::Branch(insn));
}
}
} else {
if f.is_branch(insn) || f.is_ret(insn) {
return Err(RegAllocError::BB(block));
}
}
}
}
// Check that the entry block has no block args: otherwise it is
// undefined what their value would be.
if f.block_params(f.entry_block()).len() > 0 {
return Err(RegAllocError::BB(f.entry_block()));
}
Ok(())
}