passes: implement control-flow pass (4 schemes + rotation)

FlowPass rebuilds one method per scheme picked from FlowConfig:
state-spread (dense tableswitch dispatcher), computed-switch
(sparse lookupswitch over a counter local), butterfly (branch
inversion), exception-goto (dispatcher under try with a dead
catch-all), plus opaque goto-splices inside every method and
StackMapTable recomputation afterwards. Now in standard_pipeline
(between constants and noise); --no-flow is an honest off-flag.

Acceptance is real JVM, not shapes: per-scheme verify, rotation
spread, calc-prints-45 execution, paranoid smoke on lorep-1.1.0
(131 methods rebuilt, no new failures over standard).

Two verifier rules learned the hard way are now construction
invariants: no inserted mark shares a bytecode offset with a
foreign one (nop pads), and no frameless gap follows a terminal
(dead blocks are opaque-entered and throw).

Also fixes a latent frame-emitter bug the pass exposed: wide
stack values were written as slot pairs ([Double, Top]) while
javac writes one entry ([Double]); HotSpot rejects the pair.
Stacks now collapse like locals (emit::stack_vars), with a
regression test in classfile/tests/frames.rs.
This commit is contained in:
Loki 2026-10-09 11:11:55 +02:00
parent 39178420e4
commit 2172206e83
Signed by: boba
GPG key ID: 253067914055423B
15 changed files with 1360 additions and 103 deletions

View file

@ -35,7 +35,7 @@ pub(super) fn emit_frames(
Some(previous) => target
.checked_sub(previous)
.and_then(|delta| delta.checked_sub(1))
.ok_or_else(|| merger.invalid("кадры не по порядку"))?,
.ok_or_else(|| merger.invalid("frames out of order"))?,
};
out.push(select(delta, &state.locals, &previous_locals, &state.stack));
previous_locals = state.locals.clone();
@ -54,13 +54,17 @@ fn select(
// otherwise halves would produce `Append{[Top, Long]}`.
let current = vars(locals);
let base = vars(previous);
// The operand stack needs the same collapsing: a `long`/`double` is one
// entry (`javac` writes `[Double, Integer]`, not `[Double, Top, Integer]`);
// an explicit half without its value is rejected by HotSpot.
let stack = stack_vars(stack);
if current == base {
if stack.is_empty() {
return StackMapFrame::Same {
offset_delta: delta,
};
}
if let [local] = stack {
if let [local] = stack.as_slice() {
return StackMapFrame::SameLocals {
offset_delta: delta,
local: local.clone(),
@ -94,6 +98,30 @@ fn select(
}
}
/// Stack entries → values: `[Double, Top]` — one entry (like `javac`).
///
/// Unlike [`vars`], no trailing-`Top` trimming: every stack entry is live by
/// construction, and dropping one would corrupt the depth.
fn stack_vars(stack: &[VerificationType]) -> Vec<VerificationType> {
let mut out = Vec::with_capacity(stack.len());
let mut index = 0;
while index < stack.len() {
match &stack[index] {
VerificationType::Long | VerificationType::Double
if stack.get(index + 1) == Some(&VerificationType::Top) =>
{
out.push(stack[index].clone());
index += 2;
}
other => {
out.push(other.clone());
index += 1;
}
}
}
out
}
/// Slots → variables: `[Long, Top]` — one entry; trailing `Top`s — gone.
///
/// Single `Top`s inside (holes) are kept: in `Full` they are a legal type, and we