diff --git a/front/hir/tests/asm_result_types.rs b/front/hir/tests/asm_result_types.rs new file mode 100644 index 00000000..0868345f --- /dev/null +++ b/front/hir/tests/asm_result_types.rs @@ -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 { + 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 = [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}" + ); + } +} diff --git a/front/parser/src/verification/expression.rs b/front/parser/src/verification/expression.rs index ba3bbe9b..3d1a6eae 100644 --- a/front/parser/src/verification/expression.rs +++ b/front/parser/src/verification/expression.rs @@ -602,15 +602,47 @@ impl Validator<'_> { fn validate_expr_asm_block( &mut self, expression: &Expression, - _expected: Option<&WaveType>, + expected: Option<&WaveType>, ) -> Result { 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) } } diff --git a/tests/codegen/asm_results.rs b/tests/codegen/asm_results.rs new file mode 100644 index 00000000..0a7905d3 --- /dev/null +++ b/tests/codegen/asm_results.rs @@ -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}"); + } +} diff --git a/tests/codegen_regressions.rs b/tests/codegen_regressions.rs index 13b03953..3239e3e4 100644 --- a/tests/codegen_regressions.rs +++ b/tests/codegen_regressions.rs @@ -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"]