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
74 changes: 69 additions & 5 deletions front/hir/src/conversions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<WaveType> {
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<Option<NumericExpressionInfo>> {
let mut planner =
Planner { program, facts: vec![None; program.expression_count()], seen: HashSet::new() };
Expand All @@ -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(_)))
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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())?
};
Expand Down Expand Up @@ -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; }";
Expand Down
8 changes: 8 additions & 0 deletions front/parser/src/verification/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand Down
9 changes: 7 additions & 2 deletions llvm/src/codegen/arithmetic_runtime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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();
Expand Down Expand Up @@ -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
};
Expand Down Expand Up @@ -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."));
Expand Down
13 changes: 11 additions & 2 deletions llvm/src/codegen/ir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down
11 changes: 11 additions & 0 deletions llvm/src/statement/control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
58 changes: 58 additions & 0 deletions tests/cases/shared/test140.wave
Original file line number Diff line number Diff line change
@@ -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;
}
43 changes: 43 additions & 0 deletions tests/cases/shared/test141.wave
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// wave-test: mode=run
// #914: array returns copy values rather than leaking their local address.
fun local() -> array<u8, 4> {
var bytes: array<u8, 4> = [7, 8, 9, 10];
return (bytes);
}

fun identity(bytes: array<u8, 4>) -> array<u8, 4> {
bytes[0] = 11;
return bytes;
}

fun literal() -> array<u8, 4> {
return [3, 4, 5, 6];
}

fun matrix() -> array<array<i32, 2>, 2> {
var cells: array<array<i32, 2>, 2> = [[1, 2], [3, 4]];
return cells;
}

fun empty() -> array<u8, 0> {
var bytes: array<u8, 0> = [];
return bytes;
}

fun main() -> i32 {
var a: array<u8, 4> = local();
var b: array<u8, 4> = identity(a);
var c: array<u8, 4> = 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<array<i32, 2>, 2> = matrix();
if (cells[0][0] != 1 || cells[1][1] != 4) {
return 3;
}
var zero: array<u8, 0> = empty();
return 0;
}
71 changes: 71 additions & 0 deletions tests/codegen_regressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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");
Expand Down
Loading
Loading