diff --git a/front/hir/src/conversions.rs b/front/hir/src/conversions.rs index de8687c8..c4a0b079 100644 --- a/front/hir/src/conversions.rs +++ b/front/hir/src/conversions.rs @@ -17,6 +17,7 @@ use super::{ExpressionId, HirExpressionType, TypedProgram}; use error::SourceSpan; use parser::ast::{AssignOperator, Expression, Literal, Operator, WaveType}; use std::collections::HashSet; +use utils::const_int::ConstInt; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ConversionMode { @@ -216,6 +217,35 @@ fn literal(expr: &Expression) -> bool { } } +/// Truthiness observes the whole untyped token before any integer narrowing. +/// Keep the usual i32 representation when it fits, then use an existing Wave +/// width that preserves the value. Typed operands retain their declared width. +fn boolean_literal_type(mut expr: &Expression) -> Option { + let mut negative = false; + loop { + match expr.unspanned() { + Expression::Grouped(inner) => expr = inner, + Expression::Unary { operator: Operator::Neg, expr: inner } => { + negative = !negative; + expr = inner; + }, + Expression::Literal(Literal::Int(raw)) => { + let token = lexer::number::IntegerLiteral::parse(raw)?; + let mut value = ConstInt::from_digits(&token.digits, token.radix)?; + if negative ^ token.negative { + value = value.negated(); + } + return [32, 64, 128, 256, 512, 1024] + .into_iter() + .find(|&bits| value.fits(bits, true)) + .map(WaveType::Int) + .or_else(|| value.fits(1024, false).then_some(WaveType::Uint(1024))); + }, + _ => return None, + } + } +} + pub(super) fn build(program: &TypedProgram) -> Vec> { let mut planner = Planner { program, facts: vec![None; program.expression_count()], seen: HashSet::new() }; @@ -236,9 +266,11 @@ 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) && **t != WaveType::Bool) - .cloned() - .unwrap_or(WaveType::Int(32)), + if hint == Some(&WaveType::Bool) { + boolean_literal_type(expr).unwrap_or(WaveType::Int(1024)) + } else { + hint.filter(|t| numeric(t)).cloned().unwrap_or(WaveType::Int(32)) + }, ), HirExpressionType::FloatLiteral => Some( hint.filter(|t| matches!(t, WaveType::Float(_))) @@ -318,7 +350,7 @@ impl Planner<'_> { } else { literal(inner).then(|| { if target_type == &WaveType::Bool { - WaveType::Int(1024) + boolean_literal_type(inner).unwrap_or(WaveType::Int(1024)) } else if pointer(target_type) { WaveType::Int(64) } else { @@ -429,7 +461,7 @@ impl Planner<'_> { }, Expression::Unary { operator, expr: inner } => { let input = if matches!(operator, Operator::Not | Operator::LogicalNot) { - self.plan(inner, None, None)? + self.plan(inner, None, boolean_literal_type(inner))? } else { self.plan(inner, None, hint.cloned())? }; @@ -702,6 +734,38 @@ fun check(n: i64, f: f64) { assert!(p.verify_conversions().is_err()); } + #[test] + fn boolean_literal_widths_preserve_values_without_changing_typed_wrap() { + for (literal, expected) in [ + ("0", WaveType::Int(32)), + ("2147483647", WaveType::Int(32)), + ("2147483648", WaveType::Int(64)), + ("4294967296", WaveType::Int(64)), + ("9223372036854775808", WaveType::Int(128)), + ("18446744073709551616", WaveType::Int(128)), + ("170141183460469231731687303715884105728", WaveType::Int(256)), + ] { + for source in [format!("{literal} as bool"), format!("true && ({literal})")] { + let p = program(&format!("fun check() {{ var value: bool = {source}; }}")); + p.verify_conversions().unwrap(); + let expression = initializer(&p, 0); + let operand = match expression { + Expression::Cast { expr, .. } => expr.as_ref(), + Expression::BinaryExpression { right, .. } => { + let Expression::Grouped(inner) = right.as_ref() else { panic!() }; + inner.as_ref() + }, + _ => panic!(), + }; + assert_eq!(p.numeric_expression_of(operand).unwrap().evaluation_type, expected); + } + } + let p = program("fun check() { var value: bool = (4294967296 as i32) as bool; }"); + let fact = p.numeric_expression_of(initializer(&p, 0)).unwrap(); + assert_eq!(fact.evaluation_type, WaveType::Int(32)); + assert_eq!(fact.conversions[0].source_type, WaveType::Int(32)); + } + #[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; }"; diff --git a/front/parser/src/verification/expression.rs b/front/parser/src/verification/expression.rs index 323fe412..b5c26fec 100644 --- a/front/parser/src/verification/expression.rs +++ b/front/parser/src/verification/expression.rs @@ -211,6 +211,14 @@ impl Validator<'_> { SemanticSpanKind::Keyword, operator_source_symbol(operator).unwrap_or("binary operator"), ); + if matches!(operator, Operator::LogicalAnd | Operator::LogicalOr) { + // Logical operands are independently tested for nonzero. Neither + // operand supplies a calculation width for the other, and the + // boolean result does not narrow an untyped integer token. + let left_type = self.validate_expr(left)?; + let right_type = self.validate_expr(right)?; + return infer_binary_type(self.program, operator, left_type, right_type); + } if matches!(operator, Operator::ShiftLeft | Operator::ShiftRight) { let left_type = self.validate_expr_expected(left, expected)?; let right_type = self.validate_expr(right)?; diff --git a/llvm/src/codegen/arithmetic_runtime/mod.rs b/llvm/src/codegen/arithmetic_runtime/mod.rs index bbcd56b0..f02021b7 100644 --- a/llvm/src/codegen/arithmetic_runtime/mod.rs +++ b/llvm/src/codegen/arithmetic_runtime/mod.rs @@ -8,7 +8,7 @@ use inkwell::{ context::Context, memory_buffer::MemoryBuffer, module::{Linkage, Module}, - values::{AnyValue, BasicValue, InstructionOpcode, InstructionValue}, + values::{AnyValue, BasicValue, FunctionValue, InstructionOpcode, InstructionValue}, }; use std::collections::HashMap; @@ -18,6 +18,7 @@ pub(super) fn lower<'ctx>( context: &'ctx Context, module: &Module<'ctx>, target: CodegenTarget, + mut prepare_helper: impl FnMut(FunctionValue<'ctx>), ) -> Result<(), String> { let builder = context.create_builder(); let mut helpers = HashMap::new(); @@ -54,6 +55,10 @@ pub(super) fn lower<'ctx>( module.link_in_module(helper).map_err(|e| e.to_string())?; let function = module.get_function(&name).expect("linked arithmetic helper"); function.set_linkage(Linkage::Private); + // Helpers are created after ordinary functions receive their + // code-generation attributes. They obey the same target and + // freestanding contract, including interrupt-safe stack use. + prepare_helper(function); helpers.insert(key.clone(), function); function }; @@ -151,7 +156,7 @@ mod tests { b"define <16 x i16> @vector(<16 x i16> %a) { %result = udiv <16 x i16> %a, splat (i16 251)\n ret <16 x i16> %result }", "vector-runtime-regression", )).unwrap(); - lower(&context, &module, CodegenTarget::FreeBsdX86_64).unwrap(); + lower(&context, &module, CodegenTarget::FreeBsdX86_64, |_| {}).unwrap(); let ir = module.print_to_string().to_string(); assert!(ir.contains("udiv <16 x i16>")); assert!(!ir.contains("__wave.runtime.")); diff --git a/llvm/src/codegen/ir.rs b/llvm/src/codegen/ir.rs index f0eed73d..4b98b19d 100644 --- a/llvm/src/codegen/ir.rs +++ b/llvm/src/codegen/ir.rs @@ -1382,8 +1382,17 @@ fn build_module( codegen_trace("skip optimization passes"); } - super::arithmetic_runtime::lower(context, module, abi_target) - .map_err(|e| CodegenError::new(CodegenPhase::Lowering, "lower arithmetic runtime", e))?; + super::arithmetic_runtime::lower(context, module, abi_target, |function| { + apply_function_codegen_attrs( + context, + function, + disable_red_zone, + abi_target, + cpu, + features, + ); + }) + .map_err(|e| CodegenError::new(CodegenPhase::Lowering, "lower arithmetic runtime", e))?; codegen_trace("finish module"); Ok(GeneratedModule { module, target_machine: tm }) diff --git a/llvm/src/statement/control.rs b/llvm/src/statement/control.rs index 36c9523c..50b7754f 100644 --- a/llvm/src/statement/control.rs +++ b/llvm/src/statement/control.rs @@ -1004,6 +1004,17 @@ pub(super) fn gen_return_ir<'ctx>( extern_c_info, ); + // Array expressions retain their storage address for indexing and + // pointer uses. A value return needs the aggregate itself, just as + // an array literal or an array argument already does. + if let ( + BasicValueEnum::PointerValue(address), + inkwell::types::BasicTypeEnum::ArrayType(array), + ) = (v, ret_ty) + { + v = builder.build_load(array, address, "ret_array").unwrap(); + } + if v.get_type() != ret_ty { v = coerce_basic_value( context, diff --git a/tests/cases/shared/test140.wave b/tests/cases/shared/test140.wave new file mode 100644 index 00000000..9320da84 --- /dev/null +++ b/tests/cases/shared/test140.wave @@ -0,0 +1,58 @@ +// wave-test: mode=run +// #913: truthiness checks an untyped integer's value before narrowing. +const NONZERO: bool = 4294967296 as bool; +const ZERO: bool = 0 as bool; + +fun main() -> i32 { + if (!NONZERO || ZERO) { + return 1; + } + if (!(4294967296 as bool) || !4294967296) { + return 2; + } + var entered: i32 = 0; + if ((4294967296)) { + entered += 1; + } + if (-4294967296) { + entered += 1; + } + if (18446744073709551616) { + entered += 1; + } + if (340282366920938463463374607431768211455) { + entered += 1; + } + while (4294967296) { + entered += 1; + break; + } + for (var i: i32 = 0; 4294967296; i += 1) { + entered += 1; + break; + } + if (true && 4294967296) { + entered += 1; + } + if (false || 4294967296) { + entered += 1; + } + if (entered != 8) { + return 3; + } + if (0 || !0 || 4294967296) { + entered += 1; + } + // A typed cast still discards high bits before the bool conversion. + if (4294967296 as i32) { + return 4; + } + // The invalid shift stays unexecuted because the left literal is nonzero. + if (!(4294967296 || ((1 as i32) << 32))) { + return 5; + } + if (entered != 9) { + return 6; + } + return 0; +} diff --git a/tests/cases/shared/test141.wave b/tests/cases/shared/test141.wave new file mode 100644 index 00000000..60d9185e --- /dev/null +++ b/tests/cases/shared/test141.wave @@ -0,0 +1,43 @@ +// wave-test: mode=run +// #914: array returns copy values rather than leaking their local address. +fun local() -> array { + var bytes: array = [7, 8, 9, 10]; + return (bytes); +} + +fun identity(bytes: array) -> array { + bytes[0] = 11; + return bytes; +} + +fun literal() -> array { + return [3, 4, 5, 6]; +} + +fun matrix() -> array, 2> { + var cells: array, 2> = [[1, 2], [3, 4]]; + return cells; +} + +fun empty() -> array { + var bytes: array = []; + return bytes; +} + +fun main() -> i32 { + var a: array = local(); + var b: array = identity(a); + var c: array = literal(); + if (a[0] != 7 || a[3] != 10 || b[0] != 11 || b[3] != 10) { + return 1; + } + if (c[0] != 3 || c[3] != 6) { + return 2; + } + var cells: array, 2> = matrix(); + if (cells[0][0] != 1 || cells[1][1] != 4) { + return 3; + } + var zero: array = empty(); + return 0; +} diff --git a/tests/codegen_regressions.rs b/tests/codegen_regressions.rs index 955bfc5f..a911bd41 100644 --- a/tests/codegen_regressions.rs +++ b/tests/codegen_regressions.rs @@ -134,6 +134,19 @@ fn contextual_float_signedness_executes_at_o0_and_o2() { assert!(ir.contains("fptosi double"), "{ir}"); } +#[test] +fn untyped_integer_truthiness_preserves_nonzero_values_at_o0_and_o2() { + run_shared_case_at_both_optimization_levels("test140"); +} + +#[test] +fn fixed_array_returns_preserve_caller_observed_values_at_o0_and_o2() { + let ir = run_shared_case_at_both_optimization_levels("test141"); + assert!(ir.contains("ret_array = load [4 x i8]"), "{ir}"); + assert!(ir.contains("load [2 x [2 x i32]]"), "{ir}"); + assert!(ir.contains("load [0 x i8]"), "{ir}"); +} + #[test] fn compact_variants_preserve_constants_statics_and_nested_payloads() { run_shared_case_at_both_optimization_levels("test126"); @@ -4170,6 +4183,64 @@ fun leaf(a: i64, b: i64, c: i64, d: i64, e: i64) -> i64 { ); } +#[test] +fn arithmetic_helpers_keep_target_and_freestanding_function_attributes() { + let dir = temp_case_dir("runtime-function-attributes"); + let source = write_wave( + &dir, + "runtime.wave", + r#" +fun divide(a: u128, b: u128) -> u128 { return a / b; } +fun remainder(a: u128, b: u128) -> u128 { return a % b; } +fun convert(a: u128) -> f64 { return a as f64; } +"#, + ); + for (mode, target, freestanding) in [ + ("bare", "x86_64-unknown-none-elf", false), + ("explicit", "x86_64-unknown-linux-gnu", true), + ("hosted", "x86_64-unknown-linux-gnu", false), + ] { + for optimization in ["-O0", "-O2"] { + let out = dir.join(mode).join(optimization); + let mut command = wavec_command(); + command + .arg("build") + .arg(&source) + .arg("--target") + .arg(target) + .args(["--cpu=x86-64-v3", "--features=+avx2", optimization, "--emit=ir,obj"]) + .arg("--out-dir") + .arg(&out); + if freestanding { + command.arg("--freestanding"); + } + let output = command.output().unwrap(); + assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); + let ir = fs::read_to_string(out.join("runtime.ll")).unwrap(); + let helpers: Vec<_> = + ir.lines().filter(|line| line.starts_with("define private")).collect(); + assert_eq!(helpers.len(), 3, "{ir}"); + for helper in helpers { + let group = helper.split_whitespace().find(|word| word.starts_with('#')).unwrap(); + let prefix = format!("attributes {group} ="); + let attributes = ir.lines().find(|line| line.starts_with(&prefix)).unwrap(); + assert!( + attributes.contains("\"target-cpu\"=\"x86-64-v3\""), + "{helper}: {attributes}" + ); + assert!( + attributes.contains("\"target-features\"=\"+avx2\""), + "{helper}: {attributes}" + ); + assert_eq!(attributes.contains("noredzone"), mode != "hosted", "{attributes}"); + if mode != "hosted" { + assert!(attributes.contains("nounwind"), "{attributes}"); + } + } + } + } +} + #[test] fn inline_asm_requires_explicit_stack_contract() { let dir = temp_case_dir("asm-stack-contract"); diff --git a/tests/whale_backend.rs b/tests/whale_backend.rs index ca531200..b85d0b6f 100644 --- a/tests/whale_backend.rs +++ b/tests/whale_backend.rs @@ -140,6 +140,24 @@ fun main() -> i32 { } } +#[test] +fn untyped_integer_condition_literals_retain_their_values_in_whale_ir() { + let case = Case::new( + "literal-truthiness", + r#" +fun main() -> i32 { + if (4294967296 as i32) { return 1; } + if (4294967296 && -4294967296) { return 0; } + return 2; +} +"#, + ); + let ir = case.ir(); + assert!(ir.contains("const i64 4294967296"), "{ir}"); + assert!(ir.contains("icmp ne i64"), "{ir}"); + assert!(case.run(&["--whale", "check", "main.wave"]).status.success()); +} + #[test] fn void_calls_keep_effects_through_grouping_and_void_returns() { let case = Case::new( diff --git a/whale/src/codegen/tests/oracle.rs b/whale/src/codegen/tests/oracle.rs index 389a252b..6fbecb1c 100644 --- a/whale/src/codegen/tests/oracle.rs +++ b/whale/src/codegen/tests/oracle.rs @@ -195,6 +195,41 @@ fn numeric_condition_truthiness_executes_without_reinferring_hir_types() { assert_eq!(run(&ir, &[ConstValue::I(-3)]).unwrap().value, Some(ConstValue::I(0))); } +#[test] +fn integer_literal_truthiness_checks_the_value_before_any_narrowing() { + for literal in [ + "0", + "1", + "2147483648", + "4294967296", + "-4294967296", + "18446744073709551616", + "170141183460469231731687303715884105727", + ] { + let nonzero = literal != "0"; + for expression in [ + literal.to_owned(), + format!("({literal})"), + format!("{literal} as bool"), + format!("true && {literal}"), + format!("false || {literal}"), + format!("!{literal}"), + ] { + let ir = module(&format!( + "fun probe() -> i32 {{ if ({expression}) {{ return 7; }} return 0; }}" + )); + let expected = if expression.starts_with('!') { !nonzero } else { nonzero }; + assert_eq!( + run(&ir, &[]).unwrap().value, + Some(ConstValue::I(if expected { 7 } else { 0 })), + "{expression}" + ); + } + } + let ir = module("fun probe() -> bool { return (4294967296 as i32) as bool; }"); + assert_eq!(run(&ir, &[]).unwrap().value, Some(ConstValue::Bool(false))); +} + #[test] fn budgets_unsupported_operations_and_traps_have_distinct_outcomes() { let ir = module(