Skip to content
Merged
Show file tree
Hide file tree
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
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

59 changes: 55 additions & 4 deletions front/hir/src/conversions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ impl Planner<'_> {
match self.program.type_of(expr)? {
HirExpressionType::Resolved(ty) if scalar(ty) => Some(ty.clone()),
HirExpressionType::IntegerLiteral => Some(
hint.filter(|t| numeric(t))
hint.filter(|t| numeric(t) && **t != WaveType::Bool)
.cloned()
.unwrap_or(WaveType::Int(32)),
),
Expand Down Expand Up @@ -387,8 +387,8 @@ impl Planner<'_> {
fact.shift_count_type = Some(right_type);
} else if numeric(&lt) && numeric(&rt) {
if matches!(operator, Operator::LogicalAnd | Operator::LogicalOr) {
self.plan(left, None, None);
self.plan(right, None, None);
self.plan(left, Some(WaveType::Bool), None);
self.plan(right, Some(WaveType::Bool), None);
fact.evaluation_type = WaveType::Bool;
fact.result_type = WaveType::Bool;
fact.computation_type = Some(WaveType::Bool);
Expand Down Expand Up @@ -512,7 +512,10 @@ pub(super) fn verify_expression(
return Err("missing numeric computation type".into());
};
if matches!(operator, Operator::LogicalAnd | Operator::LogicalOr) {
if computation != &WaveType::Bool || fact.evaluation_type != WaveType::Bool
if computation != &WaveType::Bool
|| fact.evaluation_type != WaveType::Bool
|| left.result_type != WaveType::Bool
|| right.result_type != WaveType::Bool
{
return Err("invalid logical computation".into());
}
Expand Down Expand Up @@ -651,6 +654,54 @@ mod tests {
);
}

#[test]
fn numeric_conditions_and_logical_operands_record_ordered_bool_conversions() {
use parser::ast::StatementNode;
let mut p = program(
r#"
fun check(n: i64, f: f64) {
if (n) {} else if (f) {}
while (n) {}
var b: bool = n && 2;
}
"#,
);
p.verify_conversions().unwrap();
let ASTNode::Function(function) = &p.syntax()[0] else { panic!() };
let ASTNode::Statement(StatementNode::If { condition, else_if_blocks, .. }) =
&function.body[0]
else {
panic!()
};
let integer = p.numeric_expression_of(condition).unwrap();
assert_eq!(integer.evaluation_type, WaveType::Int(64));
assert_eq!(integer.result_type, WaveType::Bool);
assert_eq!(integer.conversions[0].kind, ConversionKind::IntegerToBool);
assert_eq!(integer.conversions[0].mode, ConversionMode::Implicit);
assert_eq!(integer.conversions[0].span.file, "conversions.wave");
let float = p.numeric_expression_of(&else_if_blocks.as_ref().unwrap()[0].0).unwrap();
assert_eq!(float.evaluation_type, WaveType::Float(64));
assert_eq!(float.conversions[0].kind, ConversionKind::FloatToBool);
let ASTNode::Statement(StatementNode::While { condition, .. }) = &function.body[1] else {
panic!()
};
assert_eq!(p.numeric_expression_of(condition).unwrap().result_type, WaveType::Bool);
let Expression::BinaryExpression { left, right, .. } = initializer(&p, 2) else { panic!() };
for operand in [left.as_ref(), right.as_ref()] {
let fact = p.numeric_expression_of(operand).unwrap();
assert_eq!(fact.result_type, WaveType::Bool);
assert_eq!(fact.conversions[0].kind, ConversionKind::IntegerToBool);
}
// A connected but omitted bool conversion is still invalid at the
// logical operand boundary; a backend must never need to guess it.
let id = p.expression_id(left).unwrap();
let fact = p.numeric_expressions[id.index()].as_mut().unwrap();
fact.conversions.clear();
fact.result_type = fact.evaluation_type.clone();
fact.context_type = None;
assert!(p.verify_conversions().is_err());
}

#[test]
fn ordered_casts_preserve_truncation_signedness_modes_and_spans() {
let source = "fun check(x: i32) { var y: i64 = x as u8 as i32; var z: u32 = x as u32; var identity: i32 = x as i32; }";
Expand Down
19 changes: 19 additions & 0 deletions front/parser/src/verification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -980,6 +980,25 @@ impl<'a> Validator<'a> {

let ty = self.validate_expr(expression)?;
if self.is_truthy_expression(&ty) {
// Record the required result without changing literal inference or
// introducing a pointer-to-bool cast into the scalar contract.
let numeric = match &ty {
ExpressionType::IntLiteral(_) | ExpressionType::FloatLiteral => true,
ExpressionType::Known(ty) => matches!(
self.program.canonical_type(ty),
WaveType::Bool
| WaveType::Int(_)
| WaveType::Uint(_)
| WaveType::Float(_)
| WaveType::Byte
| WaveType::Char
),
_ => false,
};
if numeric {
self.expected_types
.insert(expression as *const Expression as usize, WaveType::Bool);
}
return Ok(());
}

Expand Down
33 changes: 33 additions & 0 deletions llvm/src/msvc/sdk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,39 @@ fn find_library(name: &str, paths: &[PathBuf]) -> Option<PathBuf> {
paths.iter().map(|p| p.join(&file)).find(|p| p.is_file())
}

/// Resolve existing file inputs for the driver's no-clobber check. Missing
/// libraries still receive the normal SDK diagnostic in validate_arguments.
pub fn existing_link_inputs(objects: &[String], args: &[String]) -> Vec<PathBuf> {
let paths: Vec<_> =
args.iter().filter_map(|a| value(a, "/LIBPATH:")).map(PathBuf::from).collect();
let no_defaults = args.iter().any(|a| a.eq_ignore_ascii_case("/NODEFAULTLIB"));
let mut files: Vec<_> = objects.iter().map(PathBuf::from).filter(|p| p.is_file()).collect();
for arg in args {
let library = if let Some(name) = value(arg, "/DEFAULTLIB:") {
(!no_defaults).then_some(name)
} else if let Some(name) = value(arg, "/WHOLEARCHIVE:") {
Some(name)
} else if Path::new(arg).extension().is_some_and(|e| e.eq_ignore_ascii_case("lib")) {
Some(arg.as_str())
} else {
None
};
if let Some(path) = library.and_then(|name| find_library(name, &paths)) {
files.push(path);
} else if let Some(path) = value(arg, "/DEF:") {
let path = PathBuf::from(path);
if path.is_file() {
files.push(path);
}
} else if Path::new(arg).is_file() {
files.push(PathBuf::from(arg));
}
}
files.sort();
files.dedup();
files
}

/// Inspect explicitly resolved inputs before either linker is launched. Object
/// directives are still interpreted by the linker; no claim of LTO validation.
pub fn validate_arguments(target: &str, args: &[String]) -> Result<(), String> {
Expand Down
43 changes: 39 additions & 4 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1875,13 +1875,29 @@ fn validate_output_paths(
let Some(emits) = build.emit.as_set() else {
return Ok(());
};
let msvc_link = plan
.link_output
.as_ref()
.filter(|_| llvm::backend::is_windows_msvc_target(&target_triple_for_global(global)))
.map(|output| {
let (_, args) = build_linker_args(global, build, &plan.link_inputs, output);
(output, args)
});
let mut outputs = Vec::new();
for job in &plan.compile_jobs {
outputs.push((job.output.clone(), None));
}
if let Some(output) = &plan.link_output {
outputs.push((output.clone(), None));
}
if let Some((output, args)) = &msvc_link {
outputs.extend(
crate::link_outputs::MsvcOutputs::destinations(output, args)
.into_iter()
.skip(1)
.map(|path| (path, None)),
);
}
for (index, input) in inputs.iter().enumerate() {
for kind in [EmitKind::Ast, EmitKind::Ir, EmitKind::Bc, EmitKind::Asm] {
if emits.contains(&kind) && supports_emit_for_input(kind, input.kind) {
Expand All @@ -1899,15 +1915,28 @@ fn validate_output_paths(
}
}
let mut sources: Vec<PathBuf> = inputs.iter().map(|input| input.path.clone()).collect();
crate::output_guard::validate(&sources, &outputs)?;
if emits.len() == 1 && emits.contains(&EmitKind::Ast) {
return Ok(());
if let Some((_, args)) = &msvc_link {
// An object from a previous build is a compile output at this phase,
// not an input to preserve. Once regenerated, link_objects protects it
// against companion publication. Explicit command-line inputs remain
// protected by the classified input list above.
sources.extend(
llvm::msvc::sdk::existing_link_inputs(&plan.link_inputs, args)
.into_iter()
.filter(|path| !plan.compile_jobs.iter().any(|job| &job.output == path)),
);
sources.extend(llvm::msvc::sdk::existing_link_inputs(&[], &global.llvm.link_args));
}
crate::output_guard::validate(&sources, &outputs)?;
// Imported source files are compiler inputs too. Resolve them before any
// emit or object job writes an artifact, including the final link output.
if !build.dry_run {
for input in inputs.iter().filter(|input| input.kind == InputKind::Wave) {
sources.extend(runner::wave_input_paths(&input.path, &global.dep, &global.llvm));
sources.extend(if emits.len() == 1 && emits.contains(&EmitKind::Ast) {
runner::wave_ast_input_paths(&input.path, &global.dep, &global.llvm)
} else {
runner::wave_input_paths(&input.path, &global.dep, &global.llvm)
});
}
crate::output_guard::validate(&sources, &outputs)?;
}
Expand Down Expand Up @@ -2460,6 +2489,12 @@ fn link_objects(
}
let (pending, bin, args) = if llvm::backend::is_windows_msvc_target(&target) {
let (bin, mut args) = build_linker_args(global, build, objects, output);
let inputs = llvm::msvc::sdk::existing_link_inputs(objects, &args);
let outputs: Vec<_> = crate::link_outputs::MsvcOutputs::destinations(output, &args)
.into_iter()
.map(|path| (path, None))
.collect();
crate::output_guard::validate(&inputs, &outputs)?;
llvm::msvc::sdk::validate_link_inputs(&target, objects, &args)
.map_err(CliError::CommandFailed)?;
let pending = crate::link_outputs::MsvcOutputs::prepare(output, &mut args)?;
Expand Down
96 changes: 75 additions & 21 deletions src/link_outputs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,20 @@ pub(crate) struct MsvcOutputs {
}

impl MsvcOutputs {
/// Plan the same destinations as staging, before creating any output files.
pub(crate) fn destinations(output: &Path, args: &[String]) -> Vec<PathBuf> {
let mut paths = vec![output.to_owned()];
for (option, path) in companion_paths(output, args) {
paths.push(path.clone());
if option == "IMPLIB" {
paths.push(path.with_extension("exp"));
}
}
paths
}

pub(crate) fn prepare(output: &Path, args: &mut Vec<String>) -> io::Result<Self> {
let companions = companion_paths(output, args);
let mut set =
Self { outputs: Vec::new(), directories: Vec::new(), preserve_backups: false };
let image = set.stage(output)?;
Expand All @@ -46,31 +59,23 @@ impl MsvcOutputs {
}
// LINK derives the .exp basename from the import library, when it emits
// that companion. Both share their final directory and basename here.
let library = option_path(args, "IMPLIB").unwrap_or_else(|| output.with_extension("lib"));
set.rewrite_path(args, "IMPLIB", &library)?;
set.stage(&library.with_extension("exp"))?;
let pdb = option_path(args, "PDB").unwrap_or_else(|| output.with_extension("pdb"));
if !pdb.to_str().is_some_and(|name| name.eq_ignore_ascii_case("NONE")) {
set.rewrite_path(args, "PDB", &pdb)?;
if !args.iter().any(|arg| option_value(arg, "PDBALTPATH").is_some()) {
let final_pdb = set.outputs.last().unwrap().destination.to_string_lossy();
args.push(format!("/PDBALTPATH:{final_pdb}"));
}
}
for (option, default) in [
("ILK", output.with_extension("ilk")),
("MAP", output.with_extension("map")),
("MANIFESTFILE", PathBuf::from(format!("{}.manifest", output.display()))),
] {
if args.iter().any(|arg| {
for (option, destination) in companions {
let explicit = args.iter().any(|arg| {
option_value(arg, option).is_some()
|| arg.eq_ignore_ascii_case(&format!("/{option}"))
}) {
let destination = option_path(args, option).unwrap_or(default);
});
if explicit || matches!(option, "IMPLIB" | "PDB") {
set.rewrite_path(args, option, &destination)?;
} else {
// Implicit companions remain beside the staged image.
set.stage(&default)?;
set.stage(&destination)?;
}
if option == "IMPLIB" {
set.stage(&destination.with_extension("exp"))?;
}
if option == "PDB" && !args.iter().any(|arg| option_value(arg, "PDBALTPATH").is_some())
{
let final_pdb = set.outputs.last().unwrap().destination.to_string_lossy();
args.push(format!("/PDBALTPATH:{final_pdb}"));
}
}
Ok(set)
Expand Down Expand Up @@ -213,6 +218,26 @@ impl MsvcOutputs {
}
}

fn companion_paths(output: &Path, args: &[String]) -> Vec<(&'static str, PathBuf)> {
[
("IMPLIB", output.with_extension("lib")),
("PDB", output.with_extension("pdb")),
("ILK", output.with_extension("ilk")),
("MAP", output.with_extension("map")),
("MANIFESTFILE", PathBuf::from(format!("{}.manifest", output.display()))),
]
.into_iter()
.filter_map(|(option, default)| {
let path = option_path(args, option).unwrap_or(default);
if option == "PDB" && path.to_str().is_some_and(|s| s.eq_ignore_ascii_case("NONE")) {
None
} else {
Some((option, path))
}
})
.collect()
}

fn option_value<'a>(arg: &'a str, option: &str) -> Option<&'a str> {
let (name, value) = arg.strip_prefix('/').or_else(|| arg.strip_prefix('-'))?.split_once(':')?;
name.eq_ignore_ascii_case(option).then_some(value)
Expand Down Expand Up @@ -247,6 +272,35 @@ mod tests {
path
}

#[test]
fn planned_destinations_match_staging_and_keep_final_names() {
let root = directory("plan");
let image = root.join("answer.dll");
let mut args = vec![
format!("/OUT:{}", image.display()),
format!("/PDB:{}", root.join("debug.pdb").display()),
format!("/MAP:{}", root.join("symbols.map").display()),
];
let planned = MsvcOutputs::destinations(&image, &args);
let pending = MsvcOutputs::prepare(&image, &mut args).unwrap();
assert_eq!(planned.len(), pending.outputs.len());
for (plan, actual) in planned.iter().zip(&pending.outputs) {
assert_eq!(
plan.parent().unwrap().canonicalize().unwrap().join(plan.file_name().unwrap()),
actual.destination,
);
}
for output in &pending.outputs {
fs::write(&output.staged, b"new artifact").unwrap();
}
pending.commit().unwrap();
for path in planned {
assert_eq!(fs::read(path).unwrap(), b"new artifact");
}
assert_eq!(fs::read_dir(&root).unwrap().count(), 7);
fs::remove_dir_all(root).unwrap();
}

#[test]
fn publication_error_restores_previously_replaced_companions() {
let root = directory("rollback");
Expand Down
Loading
Loading