From 52c705eada77d851d0a323366a1eeeffc81b5310 Mon Sep 17 00:00:00 2001 From: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> Date: Sun, 8 Nov 2026 00:42:12 +0200 Subject: [PATCH 1/2] fix(frontend): retain scalar expression asm result types in HIR Infer the result from the validated single output rather than treating inline assembly as unknown, allowing existing ordered numeric conversions for assignments, returns and casts Reject unsupported value contexts and add semantic, HIR and codegen regressions Signed-off-by: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> --- front/hir/tests/asm_result_types.rs | 115 ++++++++++++++++++++ front/parser/src/verification/expression.rs | 40 ++++++- tests/codegen/asm_results.rs | 112 +++++++++++++++++++ tests/codegen_regressions.rs | 2 + 4 files changed, 265 insertions(+), 4 deletions(-) create mode 100644 front/hir/tests/asm_result_types.rs create mode 100644 tests/codegen/asm_results.rs diff --git a/front/hir/tests/asm_result_types.rs b/front/hir/tests/asm_result_types.rs new file mode 100644 index 00000000..e6cc78dd --- /dev/null +++ b/front/hir/tests/asm_result_types.rs @@ -0,0 +1,115 @@ +// 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 main() -> 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..b2017201 --- /dev/null +++ b/tests/codegen/asm_results.rs @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: MPL-2.0 +//! Expression asm must preserve HIR conversions in both IR and native execution. +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(); + assert!(ir.contains("sext i32") && ir.contains("sitofp i64"), "{opt}: {ir}"); + assert!(ir.contains("inline_asm_expr"), "{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"] From e86128f4a87bff189bb16d26e548a68dcbee5e7e Mon Sep 17 00:00:00 2001 From: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> Date: Sun, 8 Nov 2026 00:43:50 +0200 Subject: [PATCH 2/2] test(asm): keep conversion tests target safe and avoid optimized IR name assumptions Exercise return conversions in a non-entry function and check unoptimized casts while validating optimized assembly call emission Signed-off-by: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> --- front/hir/tests/asm_result_types.rs | 17 +++++++++++++---- tests/codegen/asm_results.rs | 7 +++++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/front/hir/tests/asm_result_types.rs b/front/hir/tests/asm_result_types.rs index e6cc78dd..0868345f 100644 --- a/front/hir/tests/asm_result_types.rs +++ b/front/hir/tests/asm_result_types.rs @@ -33,7 +33,10 @@ fun main() { program.verify_conversions().unwrap(); let extended = initializer(&program, 1); - assert_eq!(program.type_of(extended), Some(&HirExpressionType::Resolved(WaveType::Int(32)))); + 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)); @@ -43,7 +46,10 @@ fun main() { 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)))); + 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)); @@ -55,7 +61,7 @@ fun main() { #[test] fn asm_results_participate_in_explicit_casts_arithmetic_and_returns() { let source = r#" -fun main() -> i64 { +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; @@ -73,7 +79,10 @@ fun main() -> i64 { 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)))); + 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 { diff --git a/tests/codegen/asm_results.rs b/tests/codegen/asm_results.rs index b2017201..0a7905d3 100644 --- a/tests/codegen/asm_results.rs +++ b/tests/codegen/asm_results.rs @@ -1,5 +1,6 @@ // 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] @@ -39,8 +40,10 @@ fun main() -> i32 { out.as_os_str(), ]); let ir = fs::read_to_string(out.join("asm-results.ll")).unwrap(); - assert!(ir.contains("sext i32") && ir.contains("sitofp i64"), "{opt}: {ir}"); - assert!(ir.contains("inline_asm_expr"), "{opt}: {ir}"); + 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()); } }