1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
//! What a pass knows about the machine it is compiling for.
//!
//! Design: section 40.12 of `spec/optimizer/40-cost-models.md`, which writes down the interface a
//! pass asks a target through, and tamnd/rucc#655, which is the observation that no pass could
//! reach it. The cost tables have existed since the crate did and nothing outside `rucc-cost` ever
//! called `for_arch`, because a pass is handed a function and a function does not know what it is
//! being compiled for.
//!
//! # Why it sits on the analysis cache
//!
//! [`crate::Analyses`] is the one thing every pass is already handed besides the function and the
//! fuel, so putting the machine there is what makes it reachable without a fourth argument on
//! every `run`. The cache is per function and so is the machine's lifetime as far as a pass is
//! concerned, and the pipeline builds one for the module and hands out copies.
//!
//! It is a copy rather than a borrow because it is two words, a pointer to a table nobody writes
//! and the goal. A pass that wants both the machine and an analysis out of the same cache would
//! otherwise be holding two borrows of it, one of them mutable, which is a fight with the borrow
//! checker over a value cheaper to copy than to reference.
//!
//! # A target with no back end
//!
//! `rucc_cost::for_arch` answers `None` for AArch64 and RISC-V, because neither has a back end and
//! neither has a cost table, and a default table would be numbers nobody chose that every pass
//! would believe. So [`Machine::costs`] is an `Option` and a pass that needs a number has to say
//! what it does without one. What it should do is nothing, and say so as a missed remark: an
//! optimization that guessed at the cost model would be a pass tuned for a machine that is not the
//! one being compiled for.
use rucc_cost::{CostTable, Cycles, Goal, RegClass, TargetCosts, TuneFlag};
use rucc_ir::Module;
use rucc_session::OptLevel;
/// The target a pass is compiling for, and which of its two cost tables applies.
#[derive(Clone, Copy)]
pub struct Machine {
costs: Option<&'static dyn TargetCosts>,
goal: Goal,
}
impl Machine {
/// The machine a module is being compiled for at that level.
///
/// Both halves come from things the pipeline already has, which is why no flag was added for
/// this. The architecture is on the module, because a module is built for a target and says
/// so, and the goal is whether the level optimizes for size, which is one call on the level.
#[must_use]
pub fn of(module: &Module, level: OptLevel) -> Self {
Self::with(rucc_cost::for_tuple(module.tuple), Goal::for_size(level.is_size()))
}
/// The machine for costs already in hand.
///
/// What [`Machine::of`] is written in terms of, and what a caller that resolved a target some
/// other way uses. `None` is a target with no cost table, which is every architecture rucc has
/// no back end for.
#[must_use]
pub const fn with(costs: Option<&'static dyn TargetCosts>, goal: Goal) -> Self {
Self { costs, goal }
}
/// A machine nobody has a cost table for, which is what a test that does not care wants.
///
/// Named for what it is rather than called `default`, because a default machine is the thing
/// this module exists to avoid: a pass that got one silently would be optimizing for a target
/// that does not exist.
#[must_use]
pub const fn unknown() -> Self {
Self { costs: None, goal: Goal::Speed }
}
/// The costs for this target, or nothing for one with no table.
#[must_use]
pub fn costs(self) -> Option<&'static dyn TargetCosts> {
self.costs
}
/// Which table applies, which is the goal the level asked for.
#[must_use]
pub const fn goal(self) -> Goal {
self.goal
}
/// The cost table for this target and goal, or nothing for a target with no table.
#[must_use]
pub fn table(self) -> Option<&'static CostTable> {
self.costs.map(|costs| costs.table(self.goal))
}
/// What this target answers for a tuning flag, and the flag's documented default without one.
///
/// A default is right here and wrong for a number, which is the asymmetry `Tuning::untuned`
/// already records: a tuning flag is a question whose safe answer is written down, and a cost
/// is a measurement of a machine.
#[must_use]
pub fn tune(self, flag: TuneFlag) -> bool {
self.costs.is_some_and(|costs| costs.tune(flag))
}
/// How many registers of that bank the allocator hands out, or nothing without a target.
#[must_use]
pub fn allocatable(self, class: RegClass) -> Option<u32> {
self.costs.map(|costs| costs.allocatable(class))
}
/// What a branch of that predictability costs, or nothing without a target.
#[must_use]
pub fn branch_cost(self, predictable: bool) -> Option<Cycles> {
self.costs.map(|costs| costs.branch_cost(self.goal, predictable))
}
/// What the machine is called in a dump, and `unknown` for a target with no table.
#[must_use]
pub fn name(self) -> &'static str {
self.costs.map_or("unknown", TargetCosts::name)
}
}
impl std::fmt::Debug for Machine {
/// The name and the goal, because a `dyn TargetCosts` has no `Debug` and the pointer would say
/// nothing to anybody reading a dump.
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Machine({}, {})", self.name(), self.goal)
}
}
/// Test helpers, which are here rather than in `crate::testing` because that module is also read
/// by two integration tests through a `#[path]` include and so cannot name anything in the crate.
#[cfg(test)]
pub(crate) mod fixtures {
use super::Machine;
use crate::analysis::Analyses;
use rucc_cost::Goal;
/// An empty analysis cache for the target the tests are written against.
///
/// x86-64, because it is the only one with a cost table and a test that reads a cost wants a
/// real number rather than a `None` that makes the pass under test do nothing. A test that
/// means to check what a pass does without a target says so with `Machine::unknown` at the
/// call site.
pub(crate) fn analyses() -> Analyses {
Analyses::new(machine())
}
/// The machine the tests are written against, which is x86-64 optimizing for speed.
pub(crate) fn machine() -> Machine {
Machine::with(rucc_cost::for_arch(rucc_target::Arch::X86_64), Goal::Speed)
}
}
#[cfg(test)]
mod tests {
use super::Machine;
use rucc_cost::{Goal, RegClass};
#[test]
fn a_machine_nobody_has_a_table_for_answers_nothing_rather_than_a_number() {
let machine = Machine::unknown();
assert!(machine.costs().is_none());
assert!(machine.table().is_none());
assert!(machine.allocatable(RegClass::Integer).is_none());
assert!(machine.branch_cost(false).is_none());
assert_eq!(machine.name(), "unknown");
}
#[test]
fn the_two_banks_of_x86_64_are_not_the_same_size() {
// The general purpose bank gives up the stack pointer and the frame pointer and the vector
// bank gives up neither, so a loop holding only floating point values has two more
// registers of room than one holding integers. The pass that asked before this existed
// read one constant for both and got the smaller.
let machine = Machine::with(rucc_cost::for_arch(rucc_target::Arch::X86_64), Goal::Speed);
assert_eq!(machine.allocatable(RegClass::Integer), Some(12));
assert_eq!(machine.allocatable(RegClass::Float), Some(14));
}
}