Skip to content
Open
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
124 changes: 124 additions & 0 deletions front/hir/tests/asm_result_types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// SPDX-License-Identifier: MPL-2.0
//! Expression-form assembly produces typed HIR facts at its scalar boundaries.
use hir::conversions::{ConversionKind, ConversionMode};
use hir::{HirExpressionType, TypedProgram};
use lexer::Lexer;
use parser::ast::{ASTNode, Expression, StatementNode, WaveType};
use parser::parse_syntax_with_spans;

fn lower(source: &str) -> Result<TypedProgram, hir::HirLoweringError> {
let tokens = Lexer::new_with_file(source, "asm-results.wave").tokenize().unwrap();
TypedProgram::lower(parse_syntax_with_spans(&tokens).unwrap())
}

fn initializer(program: &TypedProgram, index: usize) -> &Expression {
let ASTNode::Function(function) = &program.syntax()[0] else { panic!("function") };
let ASTNode::Variable(variable) = &function.body[index] else { panic!("variable") };
variable.initial_value.as_ref().unwrap()
}

#[test]
fn typed_asm_result_retains_signed_extension_and_float_conversion() {
let program = lower(
r#"
fun main() {
var small: i32 = 0;
var extended: i64 = asm { "mov eax, 7" out("eax") small clobber("nostack") };
var wide: i64 = 0;
var floating: f64 = asm { "mov rax, 7" out("rax") wide clobber("nostack") };
}
"#,
)
.unwrap();
program.verify_conversions().unwrap();

let extended = initializer(&program, 1);
assert_eq!(
program.type_of(extended),
Some(&HirExpressionType::Resolved(WaveType::Int(32)))
);
let fact = program.numeric_expression_of(extended).unwrap();
assert_eq!(fact.evaluation_type, WaveType::Int(32));
assert_eq!(fact.result_type, WaveType::Int(64));
assert_eq!(fact.conversions.len(), 1);
assert_eq!(fact.conversions[0].kind, ConversionKind::SignExtend);
assert_eq!(fact.conversions[0].mode, ConversionMode::Implicit);
assert_eq!(fact.conversions[0].span.file, "asm-results.wave");

let floating = initializer(&program, 3);
assert_eq!(
program.type_of(floating),
Some(&HirExpressionType::Resolved(WaveType::Int(64)))
);
let fact = program.numeric_expression_of(floating).unwrap();
assert_eq!(fact.evaluation_type, WaveType::Int(64));
assert_eq!(fact.result_type, WaveType::Float(64));
assert_eq!(fact.conversions.len(), 1);
assert_eq!(fact.conversions[0].kind, ConversionKind::SignedToFloat);
assert_eq!(fact.conversions[0].mode, ConversionMode::Implicit);
}

#[test]
fn asm_results_participate_in_explicit_casts_arithmetic_and_returns() {
let source = r#"
fun compute() -> i64 {
var sink: i32 = 0;
var flag: bool = (asm { "mov eax, 7" out("eax") sink }) as bool;
var sum: i32 = asm { "mov eax, 7" out("eax") sink } + 2;
return asm { "mov eax, 7" out("eax") sink };
}
"#;
let program = lower(source).unwrap();
program.verify_conversions().unwrap();
let flag = initializer(&program, 1);
let cast = program.numeric_expression_of(flag).unwrap();
assert_eq!(cast.conversions[0].kind, ConversionKind::IntegerToBool);
assert_eq!(cast.conversions[0].mode, ConversionMode::Explicit);

let sum = initializer(&program, 2);
let fact = program.numeric_expression_of(sum).unwrap();
assert_eq!(fact.computation_type, Some(WaveType::Int(32)));
let Expression::BinaryExpression { left, .. } = sum else { panic!("arithmetic") };
assert_eq!(
program.type_of(left),
Some(&HirExpressionType::Resolved(WaveType::Int(32)))
);

let ASTNode::Function(function) = &program.syntax()[0] else { panic!("function") };
let ASTNode::Statement(StatementNode::Return(Some(value))) = &function.body[3] else {
panic!("return");
};
let fact = program.numeric_expression_of(value).unwrap();
assert_eq!(fact.evaluation_type, WaveType::Int(32));
assert_eq!(fact.result_type, WaveType::Int(64));
assert_eq!(fact.conversions[0].kind, ConversionKind::SignExtend);
}

#[test]
fn invalid_asm_value_contexts_fail_during_semantic_analysis() {
let cases = [
(
r#"fun main() { var sink: i64 = 0; var bad: bool = asm { "mov rax, 7" out("rax") sink }; }"#,
"type mismatch",
),
(
r#"fun main() { var bad: i64 = asm { "nop" }; }"#,
"without an output cannot supply a value",
),
(
r#"fun main() { var a: i64 = 0; var b: i64 = 0; var bad: i64 = asm { "nop" out("rax") a out("rdx") b }; }"#,
"exactly one output",
),
(
r#"fun main() { var a: array<i64, 1> = [0]; var bad: i64 = asm { "nop" out("rax") a }; }"#,
"output must be scalar",
),
];
for (source, expected) in cases {
let failure = lower(source).unwrap_err();
assert!(
failure.to_string().contains(expected),
"expected {expected:?}, got {failure} for {source}"
);
}
}
40 changes: 36 additions & 4 deletions front/parser/src/verification/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -602,15 +602,47 @@ impl Validator<'_> {
fn validate_expr_asm_block(
&mut self,
expression: &Expression,
_expected: Option<&WaveType>,
expected: Option<&WaveType>,
) -> Result<ExpressionType, String> {
let Expression::AsmBlock { inputs, outputs, .. } = expression else {
unreachable!("expression dispatch")
};

for (_, expression) in inputs.iter().chain(outputs.iter()) {
self.validate_expr(expression)?;
for (_, input) in inputs {
self.validate_expr(input)?;
}
match outputs.as_slice() {
[] if expected.is_none() => Ok(ExpressionType::Unknown),
[] => Err("asm expression without an output cannot supply a value".into()),
[(_, output)] => {
let actual = self.validate_expr(output)?;
let ExpressionType::Known(ty) = actual else {
return Err("asm expression output must have a concrete scalar type".into());
};
let ty = self.program.canonical_type(&ty);
if !matches!(
ty,
WaveType::Int(_)
| WaveType::Uint(_)
| WaveType::Float(_)
| WaveType::Bool
| WaveType::Byte
| WaveType::Char
| WaveType::Isz
| WaveType::Usz
| WaveType::Pointer(_)
| WaveType::String
) {
return Err(format!(
"asm expression output must be scalar, found `{}`",
display_wave_type(&ty)
));
}
// Record the actual result type, not the caller's expected
// type: HIR must plan implicit conversions in source order.
Ok(ExpressionType::Known(ty))
},
_ => Err("asm expression requires exactly one output".into()),
}
Ok(ExpressionType::Unknown)
}
}
115 changes: 115 additions & 0 deletions tests/codegen/asm_results.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// SPDX-License-Identifier: MPL-2.0
//! Expression asm must preserve HIR conversions in both IR and native execution.
#![cfg(any(feature = "llvm-target-x86", feature = "llvm-target-all"))]
use super::*;

#[test]
#[cfg(any(feature = "llvm-target-x86", feature = "llvm-target-all"))]
fn typed_expression_asm_conversions_emit_ir_and_objects() {
let dir = temp_case_dir("typed-asm-result");
let source = write_wave(
&dir,
"asm-results.wave",
r#"
fun main() -> i32 {
var narrow: i32 = 0;
var wide: i64 = asm { "mov eax, 7" out("eax") narrow clobber("nostack") };
var sink: i64 = 0;
var floating: f64 = asm { "mov rax, 7" out("rax") sink clobber("nostack") };
var flag: bool = (asm { "mov rax, 7" out("rax") sink clobber("nostack") }) as bool;
var sum: i64 = asm { "mov rax, 7" out("rax") sink clobber("nostack") } + 5;
if (wide != 7 || floating != 7.0 || !flag || sum != 12) { return 1; }
return 0;
}
"#,
);
for opt in ["-O0", "-O2"] {
run_wavec([
OsStr::new("check"),
source.as_os_str(),
OsStr::new("--target=x86_64-unknown-linux-gnu"),
]);
let out = dir.join(opt);
run_wavec([
OsStr::new("build"),
source.as_os_str(),
OsStr::new("--target=x86_64-unknown-linux-gnu"),
OsStr::new("--emit=ir,obj"),
OsStr::new(opt),
OsStr::new("--out-dir"),
out.as_os_str(),
]);
let ir = fs::read_to_string(out.join("asm-results.ll")).unwrap();
if opt == "-O0" {
assert!(ir.contains("sext i32") && ir.contains("sitofp i64"), "{opt}: {ir}");
}
assert!(ir.contains("asm sideeffect"), "{opt}: {ir}");
assert!(out.join("asm-results.o").is_file());
}
}

#[test]
#[cfg(all(
target_arch = "x86_64",
target_os = "linux",
any(feature = "llvm-target-x86", feature = "llvm-target-all")
))]
fn typed_expression_asm_conversions_execute_at_o0_and_o2() {
let dir = temp_case_dir("typed-asm-run");
let source = write_wave(
&dir,
"main.wave",
r#"
fun main() -> i32 {
var narrow: i32 = 0;
var wide: i64 = asm { "mov eax, 7" out("eax") narrow clobber("nostack") };
var sink: i64 = 0;
var floating: f64 = asm { "mov rax, 7" out("rax") sink clobber("nostack") };
var flag: bool = (asm { "mov rax, 7" out("rax") sink clobber("nostack") }) as bool;
var sum: i64 = asm { "mov rax, 7" out("rax") sink clobber("nostack") } + 5;
if (wide != 7 || floating != 7.0 || !flag || sum != 12) { return 1; }
return 0;
}
"#,
);
for opt in ["-O0", "-O2"] {
let output = wavec_command()
.arg("build")
.arg(&source)
.arg(opt)
.arg("--run")
.arg("--out-dir")
.arg(dir.join(opt))
.output()
.unwrap();
assert!(
output.status.success(),
"{opt}: {}",
String::from_utf8_lossy(&output.stderr)
);
}
}

#[test]
#[cfg(any(feature = "llvm-target-x86", feature = "llvm-target-all"))]
fn invalid_expression_asm_initializer_is_a_source_diagnostic() {
let dir = temp_case_dir("typed-asm-errors");
let source = write_wave(
&dir,
"wrong.wave",
"fun main() { var sink: i64 = 0; var invalid: bool = asm { \"mov rax, 7\" out(\"rax\") sink }; }\n",
);
for emit in ["--emit=check", "--emit=ir"] {
let output = run_wavec_raw([
OsStr::new("build"),
source.as_os_str(),
OsStr::new("--target=x86_64-unknown-linux-gnu"),
OsStr::new("--error-format=json"),
OsStr::new(emit),
]);
assert!(!output.status.success());
let diagnostic = String::from_utf8_lossy(&output.stderr);
assert!(diagnostic.contains("type mismatch"), "{diagnostic}");
assert!(!diagnostic.contains("E9001"), "{diagnostic}");
}
}
2 changes: 2 additions & 0 deletions tests/codegen_regressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ include!("codegen/support.rs");

#[path = "codegen/abi.rs"]
mod abi;
#[path = "codegen/asm_results.rs"]
mod asm_results;
#[path = "codegen/diagnostics.rs"]
mod diagnostics;
#[path = "codegen/language.rs"]
Expand Down
Loading