diff --git a/crates/compiler/src/ssa.rs b/crates/compiler/src/ssa.rs index cd47d32c..8ab0e399 100644 --- a/crates/compiler/src/ssa.rs +++ b/crates/compiler/src/ssa.rs @@ -1795,6 +1795,14 @@ impl SsaBuilder { .clone() .into_iter() .map(|ret_ty| { + // An aggregate is held by address: a scalar + // zero in its place is no address at all. + if matches!( + ret_ty, + HirType::Struct(_) | HirType::Array(_, _) | HirType::Union(_) + ) { + return self.create_value(ret_ty, HirValueKind::Undef); + } let konst = default_const_for(&ret_ty); self.create_value(ret_ty, HirValueKind::Constant(konst)) }) diff --git a/crates/compiler/src/tiered_backend.rs b/crates/compiler/src/tiered_backend.rs index af33852a..17254f7c 100644 --- a/crates/compiler/src/tiered_backend.rs +++ b/crates/compiler/src/tiered_backend.rs @@ -2820,7 +2820,7 @@ impl TieredBackend { queue.request_compile(ctx.bead_id, Some(count)) } _ => { - let code = osr::lazy_compile(ctx.bead_id); + let code = osr::try_lazy_compile(ctx.bead_id); if !code.is_null() { return Some(code); } @@ -4781,7 +4781,7 @@ fn ensure_baseline( return true; } if lazy { - return !osr::lazy_compile(bead_id).is_null() || bound.bead().compiled().is_some(); + return !osr::try_lazy_compile(bead_id).is_null() || bound.bead().compiled().is_some(); } let existing = cranelift.with_lock(|be| be.get_function_ptr(func_id)); let entry = match existing { diff --git a/crates/compiler/tests/implicit_aggregate_return.rs b/crates/compiler/tests/implicit_aggregate_return.rs new file mode 100644 index 00000000..75cc93be --- /dev/null +++ b/crates/compiler/tests/implicit_aggregate_return.rs @@ -0,0 +1,369 @@ +#![cfg(feature = "cranelift-backend")] + +//! The return the SSA builder adds where a body can fall off its end. +//! +//! A function whose every path returns still ends in a block its +//! statements never reach, and the builder closes that block with a +//! return of a placeholder value. The placeholder must have the return +//! type's shape: a struct is held by address on Cranelift, and once the +//! function is inlined its caller reads the placeholder's fields like +//! any other struct's. The program lowers from typed AST, is inlined, +//! and runs on Cranelift, LLVM and the HIR interpreter. + +use std::sync::Arc; + +use zyntax_compiler::hir::{HirConstant, HirFunction, HirId, HirModule, HirType, HirValueKind}; +use zyntax_compiler::{CompilationConfig, compile_to_hir}; +use zyntax_typed_ast::type_registry::Mutability; +use zyntax_typed_ast::typed_ast::{ + TypedBinary, TypedBlock, TypedCall, TypedIf, TypedIndex, TypedLet, TypedParameter, +}; +use zyntax_typed_ast::{ + BinaryOp, CallingConvention, InternedString, PrimitiveType, Span, Type, TypeRegistry, + TypedDeclaration, TypedExpression, TypedFunction, TypedLiteral, TypedNode, TypedProgram, + TypedStatement, Visibility, +}; + +type Expr = TypedNode; +type Stmt = TypedNode; + +fn span() -> Span { + Span::new(0, 10) +} + +fn node(inner: T, ty: Type) -> TypedNode { + TypedNode { + node: inner, + ty, + span: span(), + } +} + +fn name(s: &str) -> InternedString { + InternedString::new_global(s) +} + +fn i64_ty() -> Type { + Type::Primitive(PrimitiveType::I64) +} + +fn f64_ty() -> Type { + Type::Primitive(PrimitiveType::F64) +} + +fn bool_ty() -> Type { + Type::Primitive(PrimitiveType::Bool) +} + +fn unit_ty() -> Type { + Type::Primitive(PrimitiveType::Unit) +} + +fn pair_ty() -> Type { + Type::Tuple(vec![i64_ty(), f64_ty()]) +} + +fn int(v: i128) -> Expr { + node(TypedExpression::Literal(TypedLiteral::Integer(v)), i64_ty()) +} + +fn float(v: f64) -> Expr { + node(TypedExpression::Literal(TypedLiteral::Float(v)), f64_ty()) +} + +fn var(s: &str, ty: Type) -> Expr { + node(TypedExpression::Variable(name(s)), ty) +} + +fn bin(op: BinaryOp, l: Expr, r: Expr, ty: Type) -> Expr { + node( + TypedExpression::Binary(TypedBinary { + op, + left: Box::new(l), + right: Box::new(r), + }), + ty, + ) +} + +fn pair(a: Expr, b: Expr) -> Expr { + node(TypedExpression::Tuple(vec![a, b]), pair_ty()) +} + +fn field(object: Expr, i: i128, ty: Type) -> Expr { + node( + TypedExpression::Index(TypedIndex { + object: Box::new(object), + index: Box::new(int(i)), + }), + ty, + ) +} + +fn call(callee: &str, args: Vec, ty: Type) -> Expr { + node( + TypedExpression::Call(TypedCall { + callee: Box::new(node(TypedExpression::Variable(name(callee)), unit_ty())), + positional_args: args, + named_args: vec![], + type_args: vec![], + }), + ty, + ) +} + +fn let_(s: &str, ty: Type, init: Expr) -> Stmt { + node( + TypedStatement::Let(TypedLet { + name: name(s), + ty, + mutability: Mutability::Mutable, + initializer: Some(Box::new(init)), + span: span(), + }), + unit_ty(), + ) +} + +fn ret(e: Expr) -> Stmt { + node(TypedStatement::Return(Some(Box::new(e))), unit_ty()) +} + +fn block(statements: Vec) -> TypedBlock { + TypedBlock { + statements, + span: span(), + } +} + +fn if_(cond: Expr, then: Vec) -> Stmt { + node( + TypedStatement::If(TypedIf { + condition: Box::new(cond), + then_block: block(then), + else_block: None, + span: span(), + }), + unit_ty(), + ) +} + +fn function(fn_name: &str, return_type: Type, body: Vec) -> TypedNode { + node( + TypedDeclaration::Function(TypedFunction { + name: name(fn_name), + params: vec![TypedParameter { + name: name("a"), + ty: i64_ty(), + mutability: Mutability::Mutable, + ..Default::default() + }], + return_type, + body: Some(block(body)), + visibility: Visibility::Public, + calling_convention: CallingConvention::Rust, + ..Default::default() + }), + unit_ty(), + ) +} + +/// `pick(a)` returns `(a, 2.5)` for a positive `a` and `(7, 0.5)` +/// otherwise, each from an `if` of its own: the block after the second +/// `if` is the tail no call reaches. `run(a)` is +/// `pick(a)[0] * 10 + pick(a)[1] * 2`. +fn module() -> HirModule { + let declarations = vec![ + function( + "pick", + pair_ty(), + vec![ + if_( + bin(BinaryOp::Gt, var("a", i64_ty()), int(0), bool_ty()), + vec![ret(pair(var("a", i64_ty()), float(2.5)))], + ), + if_( + bin(BinaryOp::Le, var("a", i64_ty()), int(0), bool_ty()), + vec![ret(pair(int(7), float(0.5)))], + ), + ], + ), + function( + "run", + i64_ty(), + vec![ + let_( + "t", + pair_ty(), + call("pick", vec![var("a", i64_ty())], pair_ty()), + ), + let_( + "scaled", + f64_ty(), + bin( + BinaryOp::Mul, + field(var("t", pair_ty()), 1, f64_ty()), + float(2.0), + f64_ty(), + ), + ), + ret(bin( + BinaryOp::Add, + bin( + BinaryOp::Mul, + field(var("t", pair_ty()), 0, i64_ty()), + int(10), + i64_ty(), + ), + node( + TypedExpression::Cast(zyntax_typed_ast::typed_ast::TypedCast { + expr: Box::new(var("scaled", f64_ty())), + target_type: i64_ty(), + }), + i64_ty(), + ), + i64_ty(), + )), + ], + ), + ]; + let mut program = TypedProgram { + language: None, + declarations, + span: span(), + source_files: vec![], + type_registry: TypeRegistry::new(), + }; + let mut module = compile_to_hir( + &mut program, + Arc::new(TypeRegistry::new()), + CompilationConfig { + opt_level: 0, + debug_info: false, + enable_monomorphization: true, + memory_strategy: None, + ..Default::default() + }, + ) + .expect("the program lowers"); + // Inlined, then split into fields, as the tiers do: `run` reads the + // fields of each value `pick` returns, the placeholder's included. + zyntax_compiler::inline::run_module(&mut module); + zyntax_compiler::aggregate_scalarize::run_module(&mut module); + module +} + +fn find(module: &HirModule, fn_name: &str) -> HirId { + module + .functions + .values() + .find(|f: &&HirFunction| f.name.resolve_global().as_deref() == Some(fn_name)) + .unwrap_or_else(|| panic!("{fn_name} is in the module")) + .id +} + +/// `run(a)` for a positive and a non-positive `a`. +const PROBES: [(i64, i64); 2] = [(3, 35), (-1, 71)]; + +type Entry = extern "C" fn(i64) -> i64; + +#[test] +fn the_placeholder_has_the_return_types_shape() { + let module = module(); + let pick = &module.functions[&find(&module, "pick")]; + let mistyped: Vec = module + .functions + .values() + .flat_map(|f| f.values.values()) + .filter(|v| matches!(v.ty, HirType::Struct(_))) + .filter_map(|v| match &v.kind { + HirValueKind::Constant(c) if !matches!(c, HirConstant::Struct(_)) => { + Some(format!("{:?}: {:?} = {c:?}", v.id, v.ty)) + } + _ => None, + }) + .collect(); + assert!( + mistyped.is_empty(), + "a struct-typed value holds a scalar constant:\n{}", + mistyped.join("\n") + ); + // The tail is there to be checked: `pick` returns from three blocks. + let returns = pick + .blocks + .values() + .filter(|b| { + matches!( + b.terminator, + zyntax_compiler::hir::HirTerminator::Return { .. } + ) + }) + .count(); + assert!(returns >= 3, "pick has {returns} returns"); + assert!( + module.functions[&find(&module, "run")] + .blocks + .values() + .flat_map(|b| b.instructions.iter()) + .all(|i| !matches!( + i, + zyntax_compiler::hir::HirInstruction::Call { + callee: zyntax_compiler::hir::HirCallable::Function(_), + .. + } + )), + "pick is inlined into run" + ); +} + +#[test] +fn inlined_on_cranelift() { + use zyntax_compiler::cranelift_backend::CraneliftBackend; + let module = module(); + let mut backend = CraneliftBackend::new().expect("backend"); + backend.compile_module(&module).expect("compile"); + backend.finalize_definitions().expect("finalize"); + let ptr = backend + .get_function_ptr(find(&module, "run")) + .expect("run is compiled"); + let f: Entry = unsafe { std::mem::transmute(ptr) }; + for (a, want) in PROBES { + assert_eq!(f(a), want, "run({a})"); + } +} + +#[test] +fn inlined_on_the_interpreter() { + use zyntax_compiler::hir_interp::{HirInterpreter, value_to_i64}; + use zyntax_compiler::value::ZyntaxValue; + let module = module(); + for (a, want) in PROBES { + let mut interp = HirInterpreter::new(); + let v = interp + .call(&module, "run", vec![ZyntaxValue::Int(a)]) + .unwrap_or_else(|e| panic!("run({a}): {e:?}")); + assert_eq!(value_to_i64(&v), Some(want), "run({a}): {v:?}"); + } +} + +#[cfg(feature = "llvm-backend")] +#[test] +fn inlined_on_llvm() { + use inkwell::context::Context; + use zyntax_compiler::llvm_jit_backend::LLVMJitBackend; + if zyntax_compiler::llvm_link::find_linker().is_err() { + eprintln!("no system linker; skipping the LLVM leg"); + return; + } + let module = module(); + let context: &'static Context = Box::leak(Box::new(Context::create())); + let mut backend = LLVMJitBackend::new(context).expect("backend"); + backend.compile_module(&module).expect("compile"); + let ptr = backend + .get_function_pointer(find(&module, "run")) + .expect("run is compiled"); + let f: Entry = unsafe { std::mem::transmute(ptr) }; + for (a, want) in PROBES { + assert_eq!(f(a), want, "run({a})"); + } +} diff --git a/crates/compiler/tests/lazy_compile_declined.rs b/crates/compiler/tests/lazy_compile_declined.rs new file mode 100644 index 00000000..cbef9420 --- /dev/null +++ b/crates/compiler/tests/lazy_compile_declined.rs @@ -0,0 +1,75 @@ +//! A function the baseline cannot compile stays interpreted when a hot +//! call asks for its code. +//! +//! A call counted past the threshold with no compile worker to hand it +//! to compiles on the calling thread. A compile that fails leaves the +//! function to the interpreter; the process goes on. + +#![cfg(feature = "cranelift-backend")] + +use std::collections::HashSet; + +use zyntax_compiler::hir::{ + HirConstant, HirFunction, HirFunctionSignature, HirInstruction, HirModule, HirTerminator, + HirType, HirValueKind, +}; +use zyntax_compiler::tiered_backend::{TieredBackend, TieredConfig}; +use zyntax_typed_ast::InternedString; + +/// `fn wide() -> i64 { splat(1.0) : <8 x f32>; 0 }`: a vector wider +/// than Cranelift holds, so the baseline declines it. +fn wide_vector_function() -> HirFunction { + let signature = HirFunctionSignature { + params: vec![], + returns: vec![HirType::I64], + type_params: vec![], + const_params: vec![], + lifetime_params: vec![], + is_variadic: false, + is_async: false, + is_fiber: false, + effects: vec![], + is_pure: false, + }; + let mut f = HirFunction::new(InternedString::new_global("wide"), signature); + let f32x8 = HirType::Vector(Box::new(HirType::F32), 8); + let one = f.create_value(HirType::F32, HirValueKind::Constant(HirConstant::F32(1.0))); + let zero = f.create_value(HirType::I64, HirValueKind::Constant(HirConstant::I64(0))); + let v = f.create_value(f32x8.clone(), HirValueKind::Instruction); + let entry = f.entry_block; + let blk = f.blocks.get_mut(&entry).unwrap(); + blk.instructions.push(HirInstruction::VectorSplat { + result: v, + ty: f32x8, + scalar: one, + }); + blk.terminator = HirTerminator::Return { values: vec![zero] }; + f.attributes.optimized = true; + f.attributes.deferred = true; + f +} + +/// One test: the lazy compiler a backend installs is the process's. +#[test] +fn a_hot_call_to_a_function_that_does_not_compile_runs_interpreted() { + // No worker: the hot call compiles on its own thread. + // SAFETY: the only test in this binary, set before any thread reads + // the environment. + unsafe { std::env::set_var("ZYNTAX_DISABLE_WARM_UP", "1") }; + let function = wide_vector_function(); + let id = function.id; + let mut module = HirModule::new(InternedString::new_global("declined")); + module.functions.insert(id, function); + let config = TieredConfig { + baseline_threshold: 1, + ..TieredConfig::default() + }; + let mut backend = TieredBackend::new(config).expect("tiered backend"); + backend + .compile_module_lazily(module, None, HashSet::from([id]), HashSet::new(), false) + .expect("module compiles"); + let mut tick = backend.interpreter_tick_callback(id).expect("a tick"); + for _ in 0..3 { + assert_eq!(tick(), None, "no code for a function the baseline declines"); + } +}