Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): add option to emit IR #296

Merged
merged 1 commit into from
Jan 12, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion crates/mun/Cargo.toml
Original file line number Diff line number Diff line change
@@ -32,4 +32,3 @@ features = ["precommit-hook", "run-cargo-test", "run-cargo-fmt", "run-cargo-clip

[dev-dependencies]
tempfile = "3.1"
serial_test = "0.4"
8 changes: 7 additions & 1 deletion crates/mun/src/lib.rs
Original file line number Diff line number Diff line change
@@ -68,6 +68,11 @@ where
.possible_values(&["enable", "auto", "disable"])
.help("color text in terminal"),
)
.arg(
Arg::with_name("emit-ir")
.long("emit-ir")
.help("emits IR instead of a *.munlib")
)
.about("Compiles a local Mun file into a module"),
)
.subcommand(
@@ -97,6 +102,7 @@ where
)
.subcommand(
SubCommand::with_name("init")
.arg(Arg::with_name("path").help("the path to create a new project [default: .]").index(1))
)
.subcommand(
SubCommand::with_name("language-server")
@@ -109,7 +115,7 @@ where
("language-server", Some(matches)) => language_server(matches),
("start", Some(matches)) => start(matches).map(|_| ExitStatus::Success),
("new", Some(matches)) => new(matches),
("init", Some(_)) => init(),
("init", Some(matches)) => init(matches),
_ => unreachable!(),
},
Err(e) => {
3 changes: 3 additions & 0 deletions crates/mun/src/ops/build.rs
Original file line number Diff line number Diff line change
@@ -75,13 +75,16 @@ fn compiler_options(matches: &ArgMatches) -> Result<mun_compiler::Config, anyhow
})
.unwrap_or(DisplayColor::Auto);

let emit_ir = matches.is_present("emit-ir");

Ok(Config {
target: matches
.value_of("target")
.map_or_else(Target::host_target, Target::search)?,
optimization_lvl,
out_dir: None,
display_color,
emit_ir,
})
}

13 changes: 10 additions & 3 deletions crates/mun/src/ops/init.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
use std::fs;
use std::path::Path;
use std::{fs, path::PathBuf};

use anyhow::anyhow;
use clap::ArgMatches;

use crate::ExitStatus;

/// This method is invoked when the executable is run with the `init` argument indicating that a
/// user requested us to create a new project in the current directory.
pub fn init() -> Result<ExitStatus, anyhow::Error> {
let create_in = std::env::current_dir().expect("could not determine current working directory");
pub fn init(matches: &ArgMatches) -> Result<ExitStatus, anyhow::Error> {
let create_in = matches
.value_of("path")
.map(PathBuf::from)
.unwrap_or_else(|| {
std::env::current_dir().expect("could not determine current working directory")
});

let project_name = create_in
.file_name()
.expect("Failed to fetch name of current folder.")
21 changes: 13 additions & 8 deletions crates/mun/src/ops/new.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
use clap::ArgMatches;

use crate::ops::init::{create_dir, create_project};
use crate::ExitStatus;
use clap::ArgMatches;
use std::path::PathBuf;

/// This method is invoked when the executable is run with the `new` argument indicating that a
/// user requested us to create a new project in a new directory.
pub fn new(matches: &ArgMatches) -> Result<ExitStatus, anyhow::Error> {
let project_name = matches.value_of("path").expect(
"Path argument not found: This should be unreachable as clap requires this argument.",
);
let create_in: PathBuf = matches
.value_of("path")
.expect(
"Path argument not found: This should be unreachable as clap requires this argument.",
)
.into();

let create_in = std::env::current_dir()
.expect("could not determine current working directory")
.join(project_name);
let project_name = create_in
.file_name()
.expect("Invalid path argument.")
.to_str()
.expect("Project name is invalid UTF-8.");

if create_in.exists() {
eprint!(
66 changes: 44 additions & 22 deletions crates/mun/tests/integration.rs
Original file line number Diff line number Diff line change
@@ -1,54 +1,76 @@
use mun::run_with_args;
use mun_runtime::{invoke_fn, RuntimeBuilder};
use serial_test::serial;
use std::env::set_current_dir;
use std::ffi::OsString;
use std::path::Path;

const PROJECT_DIR: &str = "mun_projects";
const PROJECT_NAME: &str = "mun_example_project";

/// Creates a new project using `mun init` and then tests that it works.
#[test]
#[serial] // This test must be run in serial as files may conflict.
fn mun_init() {
let project = tempfile::Builder::new()
.prefix("mun_project_example")
.prefix(PROJECT_NAME)
.tempdir()
.unwrap();

set_current_dir(&project).unwrap();

let args: Vec<OsString> = vec!["mun".into(), "init".into()];
let args: Vec<OsString> = vec!["mun".into(), "init".into(), project.path().into()];
assert_eq!(run_with_args(args).unwrap(), mun::ExitStatus::Success);
build_and_run(project);
build_and_run(project.path());
}

/// Creates a new project using `mun new` and then tests that it works.
#[test]
#[serial] // This test must be run in serial as files may conflict.
fn mun_new() {
let project = tempfile::Builder::new()
.prefix("mun_projects")
.prefix(PROJECT_DIR)
.tempdir()
.unwrap();

set_current_dir(&project).unwrap();
let project_path = project.as_ref().join(PROJECT_NAME);
let args: Vec<OsString> = vec!["mun".into(), "new".into(), project_path.as_path().into()];
assert_eq!(run_with_args(args).unwrap(), mun::ExitStatus::Success);
build_and_run(&project_path);
}

/// Verifies that a newly created project can be used to emit IR.
#[test]
fn mun_emit_ir() {
let project_dir = tempfile::Builder::new()
.prefix(PROJECT_DIR)
.tempdir()
.unwrap();

let args: Vec<OsString> = vec!["mun".into(), "new".into(), "mun_project_example".into()];
let project_path = project_dir.path().join(PROJECT_NAME);

let args: Vec<OsString> = vec!["mun".into(), "new".into(), project_path.as_path().into()];
assert_eq!(run_with_args(args).unwrap(), mun::ExitStatus::Success);
dbg!(project.as_ref().join("mun_project_example").ancestors());
build_and_run(project.as_ref().join("mun_project_example"));
assert!(project_path.exists());

build(&project_path, &["--emit-ir"]);

let ir_path = project_path.join("target/mod.ll");
assert!(ir_path.is_file());
}

/// Builds and runs an newly generated mun project
fn build_and_run(project: impl AsRef<Path>) {
fn build(project: &Path, args: &[&str]) {
let args: Vec<OsString> = vec![
"mun".into(),
"build".into(),
"--manifest-path".into(),
project.as_ref().join("mun.toml").into(),
];
OsString::from("mun"),
OsString::from("build"),
OsString::from("--manifest-path"),
OsString::from(project.join("mun.toml")),
]
.into_iter()
.chain(args.into_iter().map(|&arg| arg.into()))
.collect();
assert_eq!(run_with_args(args).unwrap(), mun::ExitStatus::Success);
}

/// Builds and runs an newly generated mun project
fn build_and_run(project: &Path) {
build(project.as_ref(), &[]);

let library_path = project.as_ref().join("target/mod.munlib");
let library_path = project.join("target/mod.munlib");
assert!(library_path.is_file());

let runtime = RuntimeBuilder::new(&library_path).spawn().unwrap();
137 changes: 117 additions & 20 deletions crates/mun_codegen/src/assembly.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,70 @@
use crate::code_gen::{CodeGenContext, ModuleBuilder};
use crate::db::CodeGenDatabase;
use crate::{
code_gen::{CodeGenContext, ModuleBuilder, ObjectFile},
db::CodeGenDatabase,
};
use anyhow::anyhow;
use inkwell::context::Context;
use std::path::Path;
use std::sync::Arc;
use std::{path::Path, sync::Arc};
use tempfile::NamedTempFile;

/// An `Assembly` is a reference to a Mun library stored on disk.
/// An `Assembly` is a successfully linked module of code from one or more files.
pub struct Assembly<'db, 'ink, 'ctx> {
code_gen: &'ctx CodeGenContext<'db, 'ink>,
module: inkwell::module::Module<'ink>,
}

impl<'db, 'ink, 'ctx> Assembly<'db, 'ink, 'ctx> {
/// Constructs an assembly
pub fn new(
code_gen: &'ctx CodeGenContext<'db, 'ink>,
module: inkwell::module::Module<'ink>,
) -> Self {
Self { code_gen, module }
}

/// Tries to convert the assembly into an `ObjectFile`.
pub fn into_object_file(self) -> Result<ObjectFile, anyhow::Error> {
ObjectFile::new(
&self.code_gen.db.target(),
&self.code_gen.target_machine,
&self.module,
)
}

/// Tries to write the `Assembly`'s IR to file.
pub fn write_ir_to_file(self, output_path: &Path) -> Result<(), anyhow::Error> {
self.module
.print_to_file(output_path)
.map_err(|e| anyhow!("{}", e))
}
}

/// Builds an assembly for the specified file
fn build_assembly<'db, 'ink, 'ctx>(
code_gen_context: &'ctx CodeGenContext<'db, 'ink>,
module: hir::Module,
) -> Assembly<'db, 'ink, 'ctx> {
let module_builder =
ModuleBuilder::new(code_gen_context, module).expect("could not create ModuleBuilder");

module_builder.build().expect("unable to create assembly")
}

/// A `TargetAssembly` is a reference to a Mun library stored on disk.
#[derive(Debug)]
pub struct Assembly {
pub struct TargetAssembly {
file: NamedTempFile,
}

impl PartialEq for Assembly {
impl PartialEq for TargetAssembly {
fn eq(&self, other: &Self) -> bool {
self.path().eq(other.path())
}
}

impl Eq for Assembly {}
impl Eq for TargetAssembly {}

impl Assembly {
impl TargetAssembly {
pub const EXTENSION: &'static str = "munlib";

/// Returns the current location of the assembly
@@ -33,26 +78,78 @@ impl Assembly {
}
}

/// Builds an assembly for the specified file
pub(crate) fn build_assembly(db: &dyn CodeGenDatabase, module: hir::Module) -> Arc<Assembly> {
// Construct a temporary file for the assembly
let file = NamedTempFile::new().expect("could not create temp file for shared object");

/// Builds an assembly for the specified module.
pub(crate) fn build_target_assembly(
db: &dyn CodeGenDatabase,
module: hir::Module,
) -> Arc<TargetAssembly> {
// Setup the code generation context
let inkwell_context = Context::create();
let code_gen_context = CodeGenContext::new(&inkwell_context, db);

// Construct the module
let module_builder =
ModuleBuilder::new(&code_gen_context, module).expect("could not create ModuleBuilder");
let obj_file = module_builder
.build()
// Build an assembly for the module
let assembly = build_assembly(&code_gen_context, module);

// Convert the assembly into an object file
let obj_file = assembly
.into_object_file()
.expect("unable to create object file");

// Construct a temporary file for the assembly
let file = NamedTempFile::new().expect("could not create temp file for shared object");

// Translate the object file into a shared object
obj_file
.into_shared_object(file.path())
.expect("could not link object file");

Arc::new(Assembly { file })
Arc::new(TargetAssembly { file })
}

/// An `AssemblyIR` is a reference to an IR file stored on disk.
#[derive(Debug)]
pub struct AssemblyIR {
file: NamedTempFile,
}

impl PartialEq for AssemblyIR {
fn eq(&self, other: &Self) -> bool {
self.path().eq(other.path())
}
}

impl Eq for AssemblyIR {}

impl AssemblyIR {
pub const EXTENSION: &'static str = "ll";

/// Returns the current location of the IR File.
pub fn path(&self) -> &Path {
self.file.path()
}

/// Copies the assembly to the specified location
pub fn copy_to<P: AsRef<Path>>(&self, destination: P) -> Result<(), std::io::Error> {
std::fs::copy(self.path(), destination).map(|_| ())
}
}

/// Builds an IR file for the specified module.
pub(crate) fn build_assembly_ir(db: &dyn CodeGenDatabase, module: hir::Module) -> Arc<AssemblyIR> {
// Setup the code generation context
let inkwell_context = Context::create();
let code_gen_context = CodeGenContext::new(&inkwell_context, db);

// Build an assembly for the file
let assembly = build_assembly(&code_gen_context, module);

// Construct a temporary file for the assembly
let file = NamedTempFile::new().expect("could not create temp file for shared object");

// Write the assembly's IR to disk
assembly
.write_ir_to_file(file.path())
.expect("could not write to temp file");

Arc::new(AssemblyIR { file })
}
1 change: 1 addition & 0 deletions crates/mun_codegen/src/code_gen.rs
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@ pub mod symbols;
pub use context::CodeGenContext;
pub use error::CodeGenerationError;
pub use module_builder::ModuleBuilder;
pub(crate) use object_file::ObjectFile;

/// Optimizes the specified LLVM `Module` using the default passes for the given
/// `OptimizationLevel`.
Loading