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:
parent
39178420e4
commit
2172206e83
15 changed files with 1360 additions and 103 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
//! Приёмка `compute_frames` на фикстурах (`tests/data`).
|
||||
//! Acceptance for `compute_frames`: fixtures plus emitter edge cases.
|
||||
//!
|
||||
//! Кадры сносятся и считаются заново. Судьи: детерминизм + round-trip IR +
|
||||
//! настоящий `java -Xverify:all` для классов без внешних зависимостей.
|
||||
//! (`verify_class` backend'а для приёмки не годится: он ждёт кадры в байтах,
|
||||
//! а `from_bytes` отдаёт в индексах — см. `todo.md`.)
|
||||
//! Frames are dropped and recomputed. Judges: determinism + IR round-trip +
|
||||
//! real `java -Xverify:all` for classes without external dependencies.
|
||||
//! (The backend's `verify_class` is unfit for acceptance: it expects frames
|
||||
//! in bytes while `from_bytes` hands out indices — see `todo.md`.)
|
||||
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
|
|
@ -11,9 +11,12 @@ use std::collections::BTreeMap;
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use noxis_classfile::{Hierarchy, compute_frames, format};
|
||||
use noxis_classfile::{
|
||||
Arith, BranchCond, ClassBuilder, Code, Cond, Const, Insn, Prim, SlotKind, Version,
|
||||
{Hierarchy, compute_frames, format},
|
||||
};
|
||||
|
||||
/// Карта `класс → суперкласс` для LUB.
|
||||
/// A `class → superclass` map for LUB.
|
||||
#[derive(Default)]
|
||||
struct MapHierarchy {
|
||||
supers: BTreeMap<String, String>,
|
||||
|
|
@ -25,13 +28,13 @@ impl Hierarchy for MapHierarchy {
|
|||
}
|
||||
}
|
||||
|
||||
/// Классы только на JDK — их `java` проверит полностью.
|
||||
/// JDK-only classes — `java` checks them fully.
|
||||
const JDK_ONLY: [&str; 2] = [
|
||||
"dev/loki/lorep/util/TimeUtil",
|
||||
"dev/loki/lorep/util/PaginationUtil",
|
||||
];
|
||||
|
||||
/// Фикстуры: снести кадры, посчитать заново, проверить выход.
|
||||
/// Fixtures: drop frames, recompute, check the output.
|
||||
#[test]
|
||||
fn fixtures_recompute_and_verify() {
|
||||
let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/data");
|
||||
|
|
@ -41,7 +44,7 @@ fn fixtures_recompute_and_verify() {
|
|||
.filter(|path| path.extension().is_some_and(|ext| ext == "class"))
|
||||
.collect();
|
||||
names.sort();
|
||||
assert!(!names.is_empty(), "нет фикстур в tests/data");
|
||||
assert!(!names.is_empty(), "no fixtures in tests/data");
|
||||
|
||||
let mut supers = BTreeMap::new();
|
||||
let mut parsed = Vec::new();
|
||||
|
|
@ -50,7 +53,7 @@ fn fixtures_recompute_and_verify() {
|
|||
supers.insert(class.this_class.clone(), class.super_class.clone());
|
||||
parsed.push(class);
|
||||
}
|
||||
// Без суперкласса — только `java/lang/Object`.
|
||||
// Only `java/lang/Object` has no superclass.
|
||||
supers.entry("java/lang/Object".to_string()).or_insert(None);
|
||||
let hierarchy = MapHierarchy {
|
||||
supers: supers
|
||||
|
|
@ -71,11 +74,11 @@ fn fixtures_recompute_and_verify() {
|
|||
}
|
||||
let first = compute_frames(&class.this_class, method, &hierarchy).expect("recompute");
|
||||
method.code.as_mut().expect("code").frames = first.clone();
|
||||
// Детерминизм: повторный прогон даёт те же кадры.
|
||||
// Determinism: a second run yields the same frames.
|
||||
let second = compute_frames(&class.this_class, method, &hierarchy).expect("recompute");
|
||||
assert_eq!(first, second, "недетерминированные кадры");
|
||||
assert_eq!(first, second, "nondeterministic frames");
|
||||
}
|
||||
// Round-trip: записанное читается в тот же IR (включая кадры).
|
||||
// Round-trip: written bytes read back into the same IR (frames included).
|
||||
let written = format::write(class).expect("write");
|
||||
assert_eq!(
|
||||
format::parse(&written).expect("re-parse"),
|
||||
|
|
@ -89,7 +92,85 @@ fn fixtures_recompute_and_verify() {
|
|||
let _ = std::fs::remove_dir_all(&scratch);
|
||||
}
|
||||
|
||||
/// Есть ли `java` для проверки настоящим верификатором.
|
||||
/// A join with a `double` below on the stack must emit it as ONE entry.
|
||||
///
|
||||
/// Regression: the emitter used to write the slot pair (`[Double, Top]`),
|
||||
/// like `javac` does for locals — but on the operand stack `javac` writes a
|
||||
/// bare `[Double]`, and HotSpot rejects the pair with
|
||||
/// `Inconsistent stackmap frames`.
|
||||
#[test]
|
||||
fn wide_stack_join_emits_single_entries() {
|
||||
// iload_0, i2d, iload_1, <opaque join>, i2d, ddiv, d2i, ireturn.
|
||||
// At the join the stack holds a double plus an int.
|
||||
let (dead, into) = (100, 101);
|
||||
let mut insns = vec![
|
||||
Insn::Load {
|
||||
slot: 0,
|
||||
kind: SlotKind::Int,
|
||||
},
|
||||
Insn::Convert(noxis_classfile::Convert::I2D),
|
||||
Insn::Load {
|
||||
slot: 1,
|
||||
kind: SlotKind::Int,
|
||||
},
|
||||
];
|
||||
insns.extend([
|
||||
Insn::Const(Const::Int(7)),
|
||||
Insn::Dup,
|
||||
Insn::Binary {
|
||||
prim: Prim::Int,
|
||||
op: Arith::Mul,
|
||||
},
|
||||
Insn::Const(Const::Int(7)),
|
||||
Insn::Binary {
|
||||
prim: Prim::Int,
|
||||
op: Arith::Add,
|
||||
},
|
||||
Insn::Const(Const::Int(1)),
|
||||
Insn::Binary {
|
||||
prim: Prim::Int,
|
||||
op: Arith::And,
|
||||
},
|
||||
Insn::Branch(BranchCond::Zero(Cond::Ne), dead),
|
||||
Insn::Goto(into),
|
||||
Insn::Mark(dead),
|
||||
Insn::Nop,
|
||||
Insn::Mark(into),
|
||||
Insn::Convert(noxis_classfile::Convert::I2D),
|
||||
Insn::Binary {
|
||||
prim: Prim::Double,
|
||||
op: Arith::Div,
|
||||
},
|
||||
Insn::Convert(noxis_classfile::Convert::D2I),
|
||||
Insn::Return(Some(Prim::Int)),
|
||||
]);
|
||||
let code = Code {
|
||||
max_stack: 6,
|
||||
max_locals: 2,
|
||||
insns,
|
||||
exception_table: Vec::new(),
|
||||
frames: Vec::new(),
|
||||
attributes: Vec::new(),
|
||||
};
|
||||
let class = ClassBuilder::new("demo/Wide", Some("java/lang/Object"))
|
||||
.version(Version::JAVA_21)
|
||||
.method_with_code(0x0009, "calc", "(II)I", code)
|
||||
.build();
|
||||
let hierarchy = MapHierarchy::default();
|
||||
let frames = compute_frames(&class.this_class, &class.methods[0], &hierarchy).expect("frames");
|
||||
let mut class = class;
|
||||
class.methods[0].code.as_mut().expect("code").frames = frames;
|
||||
if !java_available() {
|
||||
eprintln!("SKIP: no java");
|
||||
return;
|
||||
}
|
||||
let scratch = std::env::temp_dir().join(format!("noxis-wide-{}", std::process::id()));
|
||||
let bytes = format::write(&class).expect("write");
|
||||
java_verify(&scratch, &class, &bytes).expect("java -Xverify:all");
|
||||
let _ = std::fs::remove_dir_all(&scratch);
|
||||
}
|
||||
|
||||
/// Whether `java` is available for checks with the real verifier.
|
||||
fn java_available() -> bool {
|
||||
Command::new("java")
|
||||
.arg("-version")
|
||||
|
|
@ -97,8 +178,8 @@ fn java_available() -> bool {
|
|||
.is_ok_and(|out| out.status.success())
|
||||
}
|
||||
|
||||
/// Записать класс в дерево пакетов и прогнать `java -Xverify:all`.
|
||||
/// Успех — загрузка без ошибок верификации (`main` при этом отсутствует).
|
||||
/// Write a class into a package tree and run `java -Xverify:all`.
|
||||
/// Success means loading without verification errors (`main` may be missing).
|
||||
fn java_verify(
|
||||
scratch: &Path,
|
||||
class: &noxis_classfile::ClassFile,
|
||||
|
|
@ -118,7 +199,7 @@ fn java_verify(
|
|||
.arg(scratch)
|
||||
.arg(&dotted)
|
||||
.output()
|
||||
.map_err(|e| format!("запуск java: {e}"))?;
|
||||
.map_err(|e| format!("running java: {e}"))?;
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
for bad in [
|
||||
"VerifyError",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue