This adds ISLE support for the s390x back-end and moves lowering of most instructions to ISLE. The only instructions still remaining are calls, returns, traps, and branches, most of which will need additional support in common code. Generated code is not intended to be (significantly) different than before; any additional optimizations now made easier to implement due to the ISLE layer can be added in follow-on patches. There were a few differences in some filetests, but those are all just simple register allocation changes (and all to the better!).
494 lines
18 KiB
Rust
494 lines
18 KiB
Rust
// Build script.
|
|
//
|
|
// This program is run by Cargo when building cranelift-codegen. It is used to generate Rust code from
|
|
// the language definitions in the cranelift-codegen/meta directory.
|
|
//
|
|
// Environment:
|
|
//
|
|
// OUT_DIR
|
|
// Directory where generated files should be placed.
|
|
//
|
|
// TARGET
|
|
// Target triple provided by Cargo.
|
|
//
|
|
// The build script expects to be run from the directory where this build.rs file lives. The
|
|
// current directory is used to find the sources.
|
|
|
|
use cranelift_codegen_meta as meta;
|
|
|
|
use std::env;
|
|
use std::io::Read;
|
|
use std::process;
|
|
use std::time::Instant;
|
|
|
|
fn main() {
|
|
let start_time = Instant::now();
|
|
|
|
let out_dir = env::var("OUT_DIR").expect("The OUT_DIR environment variable must be set");
|
|
let target_triple = env::var("TARGET").expect("The TARGET environment variable must be set");
|
|
|
|
let isa_targets = meta::isa::Isa::all()
|
|
.iter()
|
|
.cloned()
|
|
.filter(|isa| {
|
|
let env_key = format!("CARGO_FEATURE_{}", isa.to_string().to_uppercase());
|
|
env::var(env_key).is_ok()
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
let isas = if isa_targets.is_empty() {
|
|
// Try to match native target.
|
|
let target_name = target_triple.split('-').next().unwrap();
|
|
let isa = meta::isa_from_arch(&target_name).expect("error when identifying target");
|
|
println!("cargo:rustc-cfg=feature=\"{}\"", isa);
|
|
vec![isa]
|
|
} else {
|
|
isa_targets
|
|
};
|
|
|
|
let cur_dir = env::current_dir().expect("Can't access current working directory");
|
|
let crate_dir = cur_dir.as_path();
|
|
|
|
println!("cargo:rerun-if-changed=build.rs");
|
|
|
|
if let Err(err) = meta::generate(&isas, &out_dir, crate_dir) {
|
|
eprintln!("Error: {}", err);
|
|
process::exit(1);
|
|
}
|
|
|
|
if env::var("CRANELIFT_VERBOSE").is_ok() {
|
|
for isa in &isas {
|
|
println!("cargo:warning=Includes support for {} ISA", isa.to_string());
|
|
}
|
|
println!(
|
|
"cargo:warning=Build step took {:?}.",
|
|
Instant::now() - start_time
|
|
);
|
|
println!("cargo:warning=Generated files are in {}", out_dir);
|
|
}
|
|
|
|
// The "Meta deterministic check" CI job runs this build script N
|
|
// times to ensure it produces the same output
|
|
// consistently. However, it runs the script in a fresh directory,
|
|
// without any of the source tree present; this breaks our
|
|
// manifest check (we need the ISLE source to be present). To keep
|
|
// things simple, we just disable all ISLE-related logic for this
|
|
// specific CI job.
|
|
#[cfg(not(feature = "completely-skip-isle-for-ci-deterministic-check"))]
|
|
{
|
|
maybe_rebuild_isle(crate_dir).expect("Unhandled failure in ISLE rebuild");
|
|
}
|
|
|
|
let pkg_version = env::var("CARGO_PKG_VERSION").unwrap();
|
|
let mut cmd = std::process::Command::new("git");
|
|
cmd.arg("rev-parse")
|
|
.arg("HEAD")
|
|
.stdout(std::process::Stdio::piped())
|
|
.current_dir(env::var("CARGO_MANIFEST_DIR").unwrap());
|
|
let version = if let Ok(mut child) = cmd.spawn() {
|
|
let mut git_rev = String::new();
|
|
child
|
|
.stdout
|
|
.as_mut()
|
|
.unwrap()
|
|
.read_to_string(&mut git_rev)
|
|
.unwrap();
|
|
let status = child.wait().unwrap();
|
|
if status.success() {
|
|
let git_rev = git_rev.trim().chars().take(9).collect::<String>();
|
|
format!("{}-{}", pkg_version, git_rev)
|
|
} else {
|
|
// not a git repo
|
|
pkg_version
|
|
}
|
|
} else {
|
|
// git not available
|
|
pkg_version
|
|
};
|
|
std::fs::write(
|
|
std::path::Path::new(&out_dir).join("version.rs"),
|
|
format!(
|
|
"/// Version number of this crate. \n\
|
|
pub const VERSION: &str = \"{}\";",
|
|
version
|
|
),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
/// Strip the current directory from the file paths, because `islec`
|
|
/// includes them in the generated source, and this helps us maintain
|
|
/// deterministic builds that don't include those local file paths.
|
|
fn make_isle_source_path_relative(
|
|
cur_dir: &std::path::PathBuf,
|
|
filename: std::path::PathBuf,
|
|
) -> std::path::PathBuf {
|
|
if let Ok(suffix) = filename.strip_prefix(&cur_dir) {
|
|
suffix.to_path_buf()
|
|
} else {
|
|
filename
|
|
}
|
|
}
|
|
|
|
/// A list of compilations (transformations from ISLE source to
|
|
/// generated Rust source) that exist in the repository.
|
|
///
|
|
/// This list is used either to regenerate the Rust source in-tree (if
|
|
/// the `rebuild-isle` feature is enabled), or to verify that the ISLE
|
|
/// source in-tree corresponds to the ISLE source that was last used
|
|
/// to rebuild the Rust source (if the `rebuild-isle` feature is not
|
|
/// enabled).
|
|
#[derive(Clone, Debug)]
|
|
struct IsleCompilations {
|
|
items: Vec<IsleCompilation>,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
struct IsleCompilation {
|
|
output: std::path::PathBuf,
|
|
inputs: Vec<std::path::PathBuf>,
|
|
}
|
|
|
|
impl IsleCompilation {
|
|
/// Compute the manifest filename for the given generated Rust file.
|
|
fn manifest_filename(&self) -> std::path::PathBuf {
|
|
self.output.with_extension("manifest")
|
|
}
|
|
|
|
/// Compute the content of the source manifest for all ISLE source
|
|
/// files that go into the compilation of one Rust file.
|
|
///
|
|
/// We store this alongside the `<generated_filename>.rs` file as
|
|
/// `<generated_filename>.manifest` and use it to verify that a
|
|
/// rebuild was done if necessary.
|
|
fn compute_manifest(&self) -> Result<String, Box<dyn std::error::Error + 'static>> {
|
|
// We use the deprecated SipHasher from std::hash in order to verify
|
|
// that ISLE sources haven't changed since the generated source was
|
|
// last regenerated.
|
|
//
|
|
// We use this instead of a stronger and more usual content hash, like
|
|
// SHA-{160,256,512}, because it's built into the standard library and
|
|
// we don't want to pull in a separate crate. We try to keep Cranelift
|
|
// crate dependencies as intentionally small as possible. In fact, we
|
|
// used to use the `sha2` crate for SHA-512 and this turns out to be
|
|
// undesirable for downstream consumers (see #3609).
|
|
//
|
|
// Why not the recommended replacement
|
|
// `std::collections::hash_map::DefaultHasher`? Because we need the
|
|
// hash to be deterministic, both between runs (i.e., not seeded with
|
|
// random state) and across Rust versions.
|
|
//
|
|
// If `SipHasher` is ever actually removed from `std`, we'll need to
|
|
// find a new option, either a very small crate or something else
|
|
// that's built-in.
|
|
#![allow(deprecated)]
|
|
use std::fmt::Write;
|
|
use std::hash::{Hasher, SipHasher};
|
|
|
|
let mut manifest = String::new();
|
|
|
|
for filename in &self.inputs {
|
|
// Our source must be valid UTF-8 for this to work, else user
|
|
// will get an error on build. This is not expected to be an
|
|
// issue.
|
|
let content = std::fs::read_to_string(filename)?;
|
|
// On Windows, source is checked out with line-endings changed
|
|
// to `\r\n`; canonicalize the source that we hash to
|
|
// Unix-style (`\n`) so hashes will match.
|
|
let content = content.replace("\r\n", "\n");
|
|
// One line in the manifest: <filename> <siphash>.
|
|
let mut hasher = SipHasher::new_with_keys(0, 0); // fixed keys for determinism
|
|
hasher.write(content.as_bytes());
|
|
let filename = format!("{}", filename.display()).replace("\\", "/");
|
|
writeln!(&mut manifest, "{} {:x}", filename, hasher.finish())?;
|
|
}
|
|
|
|
Ok(manifest)
|
|
}
|
|
}
|
|
|
|
/// Construct the list of compilations (transformations from ISLE
|
|
/// source to generated Rust source) that exist in the repository.
|
|
fn get_isle_compilations(crate_dir: &std::path::Path) -> Result<IsleCompilations, std::io::Error> {
|
|
let cur_dir = std::env::current_dir()?;
|
|
|
|
let clif_isle =
|
|
make_isle_source_path_relative(&cur_dir, crate_dir.join("src").join("clif.isle"));
|
|
let prelude_isle =
|
|
make_isle_source_path_relative(&cur_dir, crate_dir.join("src").join("prelude.isle"));
|
|
let src_isa_x64 =
|
|
make_isle_source_path_relative(&cur_dir, crate_dir.join("src").join("isa").join("x64"));
|
|
let src_isa_aarch64 =
|
|
make_isle_source_path_relative(&cur_dir, crate_dir.join("src").join("isa").join("aarch64"));
|
|
let src_isa_s390x =
|
|
make_isle_source_path_relative(&cur_dir, crate_dir.join("src").join("isa").join("s390x"));
|
|
|
|
// This is a set of ISLE compilation units.
|
|
//
|
|
// The format of each entry is:
|
|
//
|
|
// (output Rust code file, input ISLE source files)
|
|
//
|
|
// There should be one entry for each backend that uses ISLE for lowering,
|
|
// and if/when we replace our peephole optimization passes with ISLE, there
|
|
// should be an entry for each of those as well.
|
|
Ok(IsleCompilations {
|
|
items: vec![
|
|
// The x86-64 instruction selector.
|
|
IsleCompilation {
|
|
output: src_isa_x64
|
|
.join("lower")
|
|
.join("isle")
|
|
.join("generated_code.rs"),
|
|
inputs: vec![
|
|
clif_isle.clone(),
|
|
prelude_isle.clone(),
|
|
src_isa_x64.join("inst.isle"),
|
|
src_isa_x64.join("lower.isle"),
|
|
],
|
|
},
|
|
// The aarch64 instruction selector.
|
|
IsleCompilation {
|
|
output: src_isa_aarch64
|
|
.join("lower")
|
|
.join("isle")
|
|
.join("generated_code.rs"),
|
|
inputs: vec![
|
|
clif_isle.clone(),
|
|
prelude_isle.clone(),
|
|
src_isa_aarch64.join("inst.isle"),
|
|
src_isa_aarch64.join("lower.isle"),
|
|
],
|
|
},
|
|
// The s390x instruction selector.
|
|
IsleCompilation {
|
|
output: src_isa_s390x
|
|
.join("lower")
|
|
.join("isle")
|
|
.join("generated_code.rs"),
|
|
inputs: vec![
|
|
clif_isle.clone(),
|
|
prelude_isle.clone(),
|
|
src_isa_s390x.join("inst.isle"),
|
|
src_isa_s390x.join("lower.isle"),
|
|
],
|
|
},
|
|
],
|
|
})
|
|
}
|
|
|
|
/// Check the manifest for the ISLE generated code, which documents
|
|
/// what ISLE source went into generating the Rust, and if there is a
|
|
/// mismatch, either invoke the ISLE compiler (if we have the
|
|
/// `rebuild-isle` feature) or exit with an error (if not).
|
|
///
|
|
/// We do this by computing a hash of the ISLE source and checking it
|
|
/// against a "manifest" that is also checked into git, alongside the
|
|
/// generated Rust.
|
|
///
|
|
/// (Why not include the `rebuild-isle` feature by default? Because
|
|
/// the build process must not modify the checked-in source by
|
|
/// default; any checked-in source is a human-managed bit of data, and
|
|
/// we can only act as an agent of the human developer when explicitly
|
|
/// requested to do so. This manifest check is a middle ground that
|
|
/// ensures this explicit control while also avoiding the easy footgun
|
|
/// of "I changed the ISLE, why isn't the compiler updated?!".)
|
|
fn maybe_rebuild_isle(
|
|
crate_dir: &std::path::Path,
|
|
) -> Result<(), Box<dyn std::error::Error + 'static>> {
|
|
let isle_compilations = get_isle_compilations(crate_dir)?;
|
|
let mut rebuild_compilations = vec![];
|
|
|
|
for compilation in &isle_compilations.items {
|
|
for file in &compilation.inputs {
|
|
println!("cargo:rerun-if-changed={}", file.display());
|
|
}
|
|
|
|
let manifest =
|
|
std::fs::read_to_string(compilation.manifest_filename()).unwrap_or(String::new());
|
|
// Canonicalize Windows line-endings into Unix line-endings in
|
|
// the manifest text itself.
|
|
let manifest = manifest.replace("\r\n", "\n");
|
|
let expected_manifest = compilation.compute_manifest()?.replace("\r\n", "\n");
|
|
if manifest != expected_manifest {
|
|
rebuild_compilations.push((compilation, expected_manifest));
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "rebuild-isle")]
|
|
{
|
|
if !rebuild_compilations.is_empty() {
|
|
set_miette_hook();
|
|
}
|
|
let mut had_error = false;
|
|
for (compilation, expected_manifest) in rebuild_compilations {
|
|
if let Err(e) = rebuild_isle(compilation, &expected_manifest) {
|
|
eprintln!("Error building ISLE files: {:?}", e);
|
|
let mut source = e.source();
|
|
while let Some(e) = source {
|
|
eprintln!("{:?}", e);
|
|
source = e.source();
|
|
}
|
|
had_error = true;
|
|
}
|
|
}
|
|
|
|
if had_error {
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
|
|
#[cfg(not(feature = "rebuild-isle"))]
|
|
{
|
|
if !rebuild_compilations.is_empty() {
|
|
for (compilation, _) in rebuild_compilations {
|
|
eprintln!("");
|
|
eprintln!(
|
|
"Error: the ISLE source files that resulted in the generated Rust source"
|
|
);
|
|
eprintln!("");
|
|
eprintln!(" * {}", compilation.output.display());
|
|
eprintln!("");
|
|
eprintln!(
|
|
"have changed but the generated source was not rebuilt! These ISLE source"
|
|
);
|
|
eprintln!("files are:");
|
|
eprintln!("");
|
|
for file in &compilation.inputs {
|
|
eprintln!(" * {}", file.display());
|
|
}
|
|
}
|
|
|
|
eprintln!("");
|
|
eprintln!("Please add `--features rebuild-isle` to your `cargo build` command");
|
|
eprintln!("if you wish to rebuild the generated source, then include these changes");
|
|
eprintln!("in any git commits you make that include the changes to the ISLE.");
|
|
eprintln!("");
|
|
eprintln!("For example:");
|
|
eprintln!("");
|
|
eprintln!(" $ cargo build -p cranelift-codegen --features rebuild-isle");
|
|
eprintln!("");
|
|
eprintln!("(This build script cannot do this for you by default because we cannot");
|
|
eprintln!("modify checked-into-git source without your explicit opt-in.)");
|
|
eprintln!("");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(feature = "rebuild-isle")]
|
|
fn set_miette_hook() {
|
|
use std::sync::Once;
|
|
static SET_MIETTE_HOOK: Once = Once::new();
|
|
SET_MIETTE_HOOK.call_once(|| {
|
|
let _ = miette::set_hook(Box::new(|_| {
|
|
Box::new(
|
|
miette::MietteHandlerOpts::new()
|
|
// This is necessary for `miette` to properly display errors
|
|
// until https://github.com/zkat/miette/issues/93 is fixed.
|
|
.force_graphical(true)
|
|
.build(),
|
|
)
|
|
}));
|
|
});
|
|
}
|
|
|
|
/// Rebuild ISLE DSL source text into generated Rust code.
|
|
///
|
|
/// NB: This must happen *after* the `cranelift-codegen-meta` functions, since
|
|
/// it consumes files generated by them.
|
|
#[cfg(feature = "rebuild-isle")]
|
|
fn rebuild_isle(
|
|
compilation: &IsleCompilation,
|
|
manifest: &str,
|
|
) -> Result<(), Box<dyn std::error::Error + 'static>> {
|
|
use cranelift_isle as isle;
|
|
|
|
// First, remove the manifest, if any; we will recreate it
|
|
// below if the compilation is successful. Ignore error if no
|
|
// manifest was present.
|
|
let manifest_filename = compilation.manifest_filename();
|
|
let _ = std::fs::remove_file(&manifest_filename);
|
|
|
|
let code = (|| {
|
|
let lexer = isle::lexer::Lexer::from_files(&compilation.inputs[..])?;
|
|
let defs = isle::parser::parse(lexer)?;
|
|
isle::compile::compile(&defs)
|
|
})()
|
|
.map_err(|e| {
|
|
// Make sure to include the source snippets location info along with
|
|
// the error messages.
|
|
|
|
let report = miette::Report::new(e);
|
|
return DebugReport(report);
|
|
|
|
struct DebugReport(miette::Report);
|
|
|
|
impl std::fmt::Display for DebugReport {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
self.0.handler().debug(&*self.0, f)
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Debug for DebugReport {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
std::fmt::Display::fmt(self, f)
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for DebugReport {}
|
|
})?;
|
|
|
|
let code = rustfmt(&code).unwrap_or_else(|e| {
|
|
println!(
|
|
"cargo:warning=Failed to run `rustfmt` on ISLE-generated code: {:?}",
|
|
e
|
|
);
|
|
code
|
|
});
|
|
|
|
println!(
|
|
"Writing ISLE-generated Rust code to {}",
|
|
compilation.output.display()
|
|
);
|
|
std::fs::write(&compilation.output, code)?;
|
|
|
|
// Write the manifest so that, in the default build configuration
|
|
// without the `rebuild-isle` feature, we can at least verify that
|
|
// no changes were made that will not be picked up. Note that we
|
|
// only write this *after* we write the source above, so no
|
|
// manifest is produced if there was an error.
|
|
std::fs::write(&manifest_filename, manifest)?;
|
|
|
|
return Ok(());
|
|
|
|
fn rustfmt(code: &str) -> std::io::Result<String> {
|
|
use std::io::Write;
|
|
|
|
let mut rustfmt = std::process::Command::new("rustfmt")
|
|
.stdin(std::process::Stdio::piped())
|
|
.stdout(std::process::Stdio::piped())
|
|
.spawn()?;
|
|
|
|
let mut stdin = rustfmt.stdin.take().unwrap();
|
|
stdin.write_all(code.as_bytes())?;
|
|
drop(stdin);
|
|
|
|
let mut stdout = rustfmt.stdout.take().unwrap();
|
|
let mut data = vec![];
|
|
stdout.read_to_end(&mut data)?;
|
|
|
|
let status = rustfmt.wait()?;
|
|
if !status.success() {
|
|
return Err(std::io::Error::new(
|
|
std::io::ErrorKind::Other,
|
|
format!("`rustfmt` exited with status {}", status),
|
|
));
|
|
}
|
|
|
|
Ok(String::from_utf8(data).expect("rustfmt always writs utf-8 to stdout"))
|
|
}
|
|
}
|