Skip to content

Commit 596dc27

Browse files
committed
LLVM entries take a Void parameter in the byte Cranelift passes
Cranelift gives a Void parameter an i8 register; the LLVM entry convention declared it as an empty struct, which takes none, so a Cranelift caller entering LLVM code (or LLVM calling Cranelift code through a cell) shifted every later argument by one register. A region outlined at a header carrying a void phi hit this once promoted. git-bug: 5ee91b7ad5f22701c646a028a90165bd85bb5e15de674e3b2c2c77e64c88febb
1 parent a569d6d commit 596dc27

5 files changed

Lines changed: 289 additions & 2 deletions

File tree

‎crates/compiler/src/abi.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
//!
1010
//! * a scalar, a pointer, a vector and a function pointer travel as
1111
//! themselves;
12+
//! * a `Void` parameter travels as a byte nothing reads, so the
13+
//! parameters after it keep their registers;
1214
//! * a struct of one scalar field travels as that scalar;
1315
//! * any other struct, and an array, travels as the address of its
1416
//! storage;

‎crates/compiler/src/llvm_backend.rs‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -642,9 +642,12 @@ impl<'ctx> LLVMBackend<'ctx> {
642642
}
643643

644644
/// The register form of a value of `ty` that travels as itself: a
645-
/// struct of one scalar field is that scalar.
645+
/// struct of one scalar field is that scalar, and `Void` the byte
646+
/// Cranelift passes for it, which nothing reads. Without that byte
647+
/// every later parameter would sit one register early.
646648
fn direct_type(&self, ty: &HirType) -> CompilerResult<BasicTypeEnum<'ctx>> {
647649
match ty {
650+
HirType::Void => Ok(self.context.i8_type().into()),
648651
HirType::Struct(s) => match crate::abi::struct_carried_as_its_field(s) {
649652
Some(field) => self.translate_type(field),
650653
None => self.translate_type(ty),
@@ -1058,6 +1061,9 @@ impl<'ctx> LLVMBackend<'ctx> {
10581061
{
10591062
self.wrap_in_struct(raw, &param.ty)?
10601063
}
1064+
// The body holds a `Void` as the empty struct, whatever
1065+
// the parameter arrived as.
1066+
HirType::Void => self.translate_type(&param.ty)?.const_zero(),
10611067
_ => raw,
10621068
};
10631069
params.push(value);
@@ -1462,7 +1468,13 @@ impl<'ctx> LLVMBackend<'ctx> {
14621468
let want: BasicTypeEnum<'ctx> = (*want).try_into().map_err(|_| {
14631469
CompilerError::CodeGen(format!("OSR re-entry parameter {i} is not a value"))
14641470
})?;
1465-
let Some(slot) = slots.get(i).copied().flatten() else {
1471+
// Nothing reads a `Void`, so none is loaded from the frame.
1472+
let Some(slot) = slots
1473+
.get(i)
1474+
.copied()
1475+
.flatten()
1476+
.filter(|slot| layout.live_in_types.get(*slot) != Some(&HirType::Void))
1477+
else {
14661478
args.push(want.const_zero().into());
14671479
continue;
14681480
};
@@ -4776,6 +4788,10 @@ impl<'ctx> LLVMBackend<'ctx> {
47764788
None => None,
47774789
};
47784790
for ((arg, ty), pass) in args.iter().zip(&callee.params).zip(&callee.abi.params) {
4791+
if *pass == Pass::Direct && *ty == HirType::Void {
4792+
arg_values.push(self.direct_type(ty)?.const_zero().into());
4793+
continue;
4794+
}
47794795
let value = self.get_value(*arg)?;
47804796
match pass {
47814797
Pass::Pointer => {
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
//! A `Void` parameter keeps the parameters after it where the other
2+
//! tier looks for them.
3+
//!
4+
//! Cranelift passes a `Void` as a byte in its own register. An LLVM
5+
//! entry standing in a call cell that Cranelift callers read, and an
6+
//! LLVM call through a cell holding Cranelift's code, must pass it the
7+
//! same way, or every later argument arrives in the register of the
8+
//! one before it.
9+
10+
#![cfg(all(feature = "cranelift-backend", feature = "llvm-backend"))]
11+
12+
use std::collections::HashSet;
13+
use std::sync::Arc;
14+
use zyntax_compiler::hir::{
15+
BinaryOp, HirBlock, HirCallable, HirConstant, HirFunction, HirFunctionSignature, HirId,
16+
HirInstruction, HirModule, HirParam, HirTerminator, HirType, HirValue, HirValueKind,
17+
ParamAttributes,
18+
};
19+
use zyntax_typed_ast::InternedString;
20+
21+
fn sig(params: Vec<HirType>, returns: Vec<HirType>) -> HirFunctionSignature {
22+
HirFunctionSignature {
23+
params: params
24+
.into_iter()
25+
.enumerate()
26+
.map(|(i, ty)| HirParam {
27+
id: HirId::new(),
28+
name: InternedString::new_global(&format!("p{i}")),
29+
ty,
30+
attributes: ParamAttributes::default(),
31+
ownership: Default::default(),
32+
})
33+
.collect(),
34+
returns,
35+
type_params: vec![],
36+
const_params: vec![],
37+
lifetime_params: vec![],
38+
is_variadic: false,
39+
is_async: false,
40+
is_fiber: false,
41+
effects: vec![],
42+
is_pure: false,
43+
}
44+
}
45+
46+
fn add_value(func: &mut HirFunction, ty: HirType, kind: HirValueKind) -> HirId {
47+
let id = HirId::new();
48+
func.values.insert(
49+
id,
50+
HirValue {
51+
id,
52+
ty,
53+
kind,
54+
uses: HashSet::new(),
55+
span: None,
56+
},
57+
);
58+
id
59+
}
60+
61+
fn konst(func: &mut HirFunction, v: i64) -> HirId {
62+
add_value(
63+
func,
64+
HirType::I64,
65+
HirValueKind::Constant(HirConstant::I64(v)),
66+
)
67+
}
68+
69+
fn body(func: &mut HirFunction) -> &mut HirBlock {
70+
let entry = func.entry_block;
71+
func.blocks.get_mut(&entry).unwrap()
72+
}
73+
74+
/// `def pick(a: i64, u: Void, b: i64, c: i64): i64 { return a*100 + b*10 + c }`
75+
fn build_pick() -> HirFunction {
76+
let mut f = HirFunction::new(
77+
InternedString::new_global("pick"),
78+
sig(
79+
vec![HirType::I64, HirType::Void, HirType::I64, HirType::I64],
80+
vec![HirType::I64],
81+
),
82+
);
83+
let a = add_value(&mut f, HirType::I64, HirValueKind::Parameter(0));
84+
let b = add_value(&mut f, HirType::I64, HirValueKind::Parameter(2));
85+
let c = add_value(&mut f, HirType::I64, HirValueKind::Parameter(3));
86+
let hundred = konst(&mut f, 100);
87+
let ten = konst(&mut f, 10);
88+
let v: Vec<HirId> = (0..4)
89+
.map(|_| add_value(&mut f, HirType::I64, HirValueKind::Instruction))
90+
.collect();
91+
let arith = |op, result, left, right| HirInstruction::Binary {
92+
op,
93+
result,
94+
ty: HirType::I64,
95+
left,
96+
right,
97+
};
98+
let blk = body(&mut f);
99+
blk.instructions.extend([
100+
arith(BinaryOp::Mul, v[0], a, hundred),
101+
arith(BinaryOp::Mul, v[1], b, ten),
102+
arith(BinaryOp::Add, v[2], v[0], v[1]),
103+
arith(BinaryOp::Add, v[3], v[2], c),
104+
]);
105+
blk.terminator = HirTerminator::Return { values: vec![v[3]] };
106+
f
107+
}
108+
109+
/// `def pick_main(): i64 { return pick(1, (), 2, 3) }`
110+
fn build_pick_main(pick_id: HirId) -> HirFunction {
111+
let mut f = HirFunction::new(
112+
InternedString::new_global("pick_main"),
113+
sig(vec![], vec![HirType::I64]),
114+
);
115+
let args: Vec<HirId> = [1, 2, 3].into_iter().map(|v| konst(&mut f, v)).collect();
116+
let unit = add_value(&mut f, HirType::Void, HirValueKind::Undef);
117+
let r = add_value(&mut f, HirType::I64, HirValueKind::Instruction);
118+
let blk = body(&mut f);
119+
blk.instructions.push(HirInstruction::Call {
120+
result: Some(r),
121+
callee: HirCallable::Function(pick_id),
122+
args: vec![args[0], unit, args[1], args[2]],
123+
type_args: vec![],
124+
const_args: vec![],
125+
is_tail: false,
126+
});
127+
blk.terminator = HirTerminator::Return { values: vec![r] };
128+
f
129+
}
130+
131+
const PICK_EXPECTED: i64 = 123;
132+
133+
fn pick_module() -> (HirModule, HirFunction, HirFunction) {
134+
let pick = build_pick();
135+
let main = build_pick_main(pick.id);
136+
let mut module = HirModule::new(InternedString::new_global("void_param"));
137+
module.functions.insert(pick.id, pick.clone());
138+
module.functions.insert(main.id, main.clone());
139+
(module, pick, main)
140+
}
141+
142+
/// One call cell, entered first by Cranelift's code for `pick` and then
143+
/// by LLVM's, from the same Cranelift caller.
144+
#[test]
145+
fn llvm_entry_takes_the_parameters_after_a_void_where_cranelift_passes_them() {
146+
use inkwell::context::Context;
147+
use zyntax_compiler::cranelift_backend::CraneliftBackend;
148+
use zyntax_compiler::llvm_jit_backend::LLVMJitBackend;
149+
150+
let (module, pick, main) = pick_module();
151+
let mut cranelift = CraneliftBackend::new().expect("backend");
152+
cranelift.set_reloadable_calls(true);
153+
cranelift.compile_module(&module).expect("compile");
154+
cranelift.finalize_definitions().expect("finalize");
155+
let ptr = cranelift.get_function_ptr(main.id).expect("main compiled");
156+
let run: unsafe extern "C" fn() -> i64 = unsafe { std::mem::transmute(ptr) };
157+
assert_eq!(unsafe { run() }, PICK_EXPECTED);
158+
159+
let context = Context::create();
160+
let mut llvm = LLVMJitBackend::new(&context).expect("backend");
161+
llvm.set_use_mcjit(true);
162+
llvm.set_module_context(Arc::new(module.clone()));
163+
llvm.set_cross_tier_links(cranelift.reload_key(), Arc::new(|_| None));
164+
llvm.compile_function(pick.id, &pick)
165+
.expect("LLVM compiles the entry");
166+
let entry = llvm.get_function_pointer(pick.id).expect("pick compiled");
167+
cranelift.publish_call_target(pick.id, entry as usize);
168+
assert_eq!(unsafe { run() }, PICK_EXPECTED);
169+
}
170+
171+
/// An LLVM entry calling Cranelift's `pick` through its cell.
172+
#[test]
173+
fn llvm_passes_the_parameters_after_a_void_where_cranelift_takes_them() {
174+
use inkwell::context::Context;
175+
use zyntax_compiler::cranelift_backend::CraneliftBackend;
176+
use zyntax_compiler::llvm_jit_backend::LLVMJitBackend;
177+
178+
let (module, _pick, main) = pick_module();
179+
let mut cranelift = CraneliftBackend::new().expect("backend");
180+
cranelift.set_reloadable_calls(true);
181+
cranelift.compile_module(&module).expect("compile");
182+
cranelift.finalize_definitions().expect("finalize");
183+
184+
let context = Context::create();
185+
let mut llvm = LLVMJitBackend::new(&context).expect("backend");
186+
llvm.set_use_mcjit(true);
187+
llvm.set_module_context(Arc::new(module.clone()));
188+
llvm.set_cross_tier_links(cranelift.reload_key(), Arc::new(|_| None));
189+
llvm.compile_function(main.id, &main)
190+
.expect("LLVM compiles the caller");
191+
let entry = llvm.get_function_pointer(main.id).expect("main compiled");
192+
let run: unsafe extern "C" fn() -> i64 = unsafe { std::mem::transmute(entry) };
193+
assert_eq!(unsafe { run() }, PICK_EXPECTED);
194+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# A driver loop whose body binds the None a call returns, so a loop
2+
# value of no type crosses its header, in a function that returns the
3+
# list it fills. The interpreted frame leaves into the region outlined
4+
# for it once the optimizing tier has compiled the region, and the
5+
# parameters after that value must arrive where the region reads them.
6+
def work(k, acc):
7+
s = acc[0]
8+
for j in range(k):
9+
s = (s * 31 + j) % 1000003
10+
acc[0] = s
11+
12+
13+
def main(n):
14+
acc = [1]
15+
times = []
16+
for i in range(n):
17+
o = work(20000000 if i >= 60 else 1000, acc)
18+
times.append(1.0 * i)
19+
times.append(1.0 * acc[0])
20+
return times
21+
22+
23+
def run(n, f):
24+
data = f(n)
25+
total = 0.0
26+
for x in data:
27+
total += x
28+
print(len(data), total)
29+
30+
31+
run(70, main)

‎crates/zyntax_python/tests/resume_points.rs‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,3 +275,47 @@ fn a_site_asked_during_an_entry_count_promotion_gets_the_optimizing_tier() {
275275
}
276276
assert!(transfers > 0, "no frame moved to a resume point in 20 runs");
277277
}
278+
279+
/// A frame leaves into a region the optimizing tier compiled, whose
280+
/// parameters include a loop value of no type: every parameter after it
281+
/// reaches the region where the region reads it, and the list the
282+
/// region returns is the one the frame filled. The frame asks while
283+
/// its loop is warm, which takes the warm-up worker; whether the region
284+
/// is promoted before the frame leaves is up to timing, so the program
285+
/// runs until it has been, each run answering as CPython does.
286+
#[cfg(feature = "llvm-backend")]
287+
#[test]
288+
fn a_region_taking_a_void_loop_value_gets_the_parameters_after_it() {
289+
let script = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/void_loop_value.py");
290+
let mut entered = false;
291+
for _ in 0..5 {
292+
let output = Command::new(env!("CARGO_BIN_EXE_zypy"))
293+
.arg("run")
294+
.arg(&script)
295+
.env("ZYPY_LLVM", "1")
296+
.env("ZYNTAX_OSR_TRACE", "1")
297+
.env_remove("ZYNTAX_DISABLE_WARM_UP")
298+
.output()
299+
.expect("zypy starts");
300+
let stdout = String::from_utf8_lossy(&output.stdout);
301+
let stderr = String::from_utf8_lossy(&output.stderr);
302+
assert!(output.status.success(), "{stderr}");
303+
assert_eq!(stdout.trim(), "71 900854.0", "{stderr}");
304+
let site = stderr
305+
.lines()
306+
.find(|l| l.starts_with("[osr] main site=") && l.ends_with("outlined resume point"))
307+
.and_then(|l| l.split("site=").nth(1))
308+
.and_then(|s| s.split(':').next());
309+
let promoted = stderr.find("(main$resume0) at tier 1");
310+
if let (Some(site), Some(promoted)) = (site, promoted)
311+
&& stderr[promoted..].contains(&format!("interpreted frame leaves at site={site} "))
312+
{
313+
entered = true;
314+
break;
315+
}
316+
}
317+
assert!(
318+
entered,
319+
"in 5 runs the frame never entered the region after its promotion"
320+
);
321+
}

0 commit comments

Comments
 (0)